Friday, August 19, 2011

Adaptive contracts for WCF

Or WCF in the raw! With my current project, I decided it would be useful to let clients request only the objects/data they will actually consume, rather than impose a specific data contract. From a solutions architecture perspective, a set of WCF services are publicly exposed for consumption, accepting and returning JSON (for reasons of brevity, given predicted limited bandwidth).

So how best to do this? Well, some research turned up this MSDN blog post....and WCF raw it shall be. I also examined OData, in depth, and I concluded that it was still immature for production use, and required too much effort to make it truly lean.

So what does a client 'see' or 'do'? From my perspective, a client should tell me what 'domain' of objects it is interested in, and potentially indicate what 'zone' is of concern. For the domain of objects, it should be possible to note the 'name' of the object property of interest, and how the client wants to refer to it. For example, I might be interested in a composite (navigated) property of x.y.z, but I want to refer to it as q. Again, this is a way of minimising bandwidth consumption. It also implies that the implied spectrum of navigation routes is well known.

All code formatted using this excellent tool: http://codeformatter.blogspot.com/2009/06/about-code-formatter.html

So an implementation uses an XML file to capture this information, an abbreviated example is shown next:

 <?xml version="1.0" encoding="utf-8" ?>  
 <Templates>  
   <TemplateCollection zone="smartphone">  
     <Template name="Accounts">  
       <Field key="Name"/>  
       <Field key="CustomisedName"/>  
       <Field key="Number"/>  
       <Field key="FinancialData.Balance.Value"   
                  returnKey="Balance"/>  
       <Field key="FinancialData.Balance"   
                   returnKey="FormattedBalance"/>  
     </Template>  
   </TemplateCollection>  
 </Templates>  

And here is a JSON request a client might make:

 {"BespokeRequest":  
   { "Specification":   
    { "Zone":"smartphone", "Name":"Accounts" }  
   }  
 }  

So it's quite obvious that 'zone' identifies a collection of domain templates - and this request is interested in the 'Accounts' domain (as shown in the XML excerpt). For the object type that comprises this domain, there are obviously a number of properties in existence, and the 'smartphone' variant template just uses what it needs. This excerpt shows the composite property mapping in use, taking a navigation specification and returning it as a simple property:

 <Field key="FinancialData.Balance.Value"   
        returnKey="Balance"/>  

So the client wants to reference FinancialData.Balance.Value as Balance. However, as the point of this exercise is to allow arbitrary specification, this is also supported, using a JSON request similar to the following:

 {"BespokeRequest":  
  { "Specification":   
   { "Name":"Accounts" },   
  {"Definitions": [  
   { "Key":"Name","Value":""},  
   { "Key":"Number","Value":""},  
   { "Key":"FinancialData.Balance.Value","Value":"Balance"},  
   { "Key":"CustomisedName","Value":""},  
   { "Key":"FinancialData.Balance.Value","Value":"FormattedBalance"}  
  ]  
  }  
 }  

If a 'Value' is null or empty, the value of 'Key' in the associated object will have a JSON name that is the same as the 'Key' string.

The rationale for the template approach is to minimise bandwidth consumption as far as possible for in house applications.

Of course we have to be able to represent this in a WCF service. For raw service methods, the return type is System.IO.Stream - which signals to the host container that the service implementation is handling the 'formatting' of the response. In my case, a number of services will be 'adaptive', so there is a mixin interface, as below:

   [ServiceContract]  
   public interface IAdaptiveService {  
     [WebInvoke(Method = "POST",   
          UriTemplate = "BespokeRequest",   
          ResponseFormat = WebMessageFormat.Json,  
          RequestFormat = WebMessageFormat.Json,   
          BodyStyle = WebMessageBodyStyle.Wrapped)]  
     [OperationContract]  
     [return: MessageParameter(Name = "BespokeResponse")]  
     Stream BespokeRequest(  
             [MessageParameter(Name = "BespokeRequest")]  
             BespokeObjectListRequest request);  
   }  

Technically, specifying the response format is unnecessary, as raw services have complete control of this aspect. The message parameter attribute is useful though, allowing a generic name to be used for the actual request being passed in.

And here are the related data contracts and supporting objects - noting that, where it makes sense, data members are allowed to be absent on deserialization - this again means fewer bytes being sent 'over the wire'.

   [DataContract(Namespace = "Integration")]  
   public class BespokeObjectListRequest {  
     [DataMember(IsRequired = true)]  
     public TemplateSpecification Specification { get; set; }  
     [DataMember(IsRequired = false)]  
     public List<FieldSet> Definitions { get; set; }  
   }  
   
   [DataContract(Namespace = "Integration")]  
   public class FieldSet {  
     [DataMember]  
     public string Key { get; set; }  
     [DataMember(IsRequired = false)]  
     public string Value { get; set; }  
   }  
   
   [DataContract(Namespace = "Integration")]  
   public class TemplateSpecification {  
     [DataMember]  
     public string Zone { get; set; }  
     [DataMember]  
     public string Name { get; set; }  
   
     public bool IsValid() {  
       return !String.IsNullOrWhiteSpace(Zone)   
                  && !String.IsNullOrWhiteSpace(Name);  
     }  
   }  

A base type provides the implementation of the IAdaptiveService interface, shown below. The implementation uses a variant of centralized exception handling, that I described in a previous post.

1:  public Stream BespokeRequest(BespokeObjectListRequest request) {  
2:   return RawExecute<MemoryStream>(GenericReponseName,  
3:    new WrappingProcessor(() => {  
4:      DBC.AssertNotNull(request, "Null request can't be interrogated");  
5:      DBC.AssertNotNull(request.Specification,   
6:        "Null request specification can't be interrogated");  
7:      DBC.Assert(request.Specification.IsValid(), "The request specification is invalid");  
8:      string targetMethod = string.Format("Bespoke{0}",   
9:        request.Specification.Name);  
10:      MethodInfo info = GetType().GetMethod(targetMethod,  
11:        BindingFlags.NonPublic | BindingFlags.Instance);  
12:      DBC.AssertNotNull(info, string.Format("Method not found - {0}", targetMethod));  
13:      return (IProcessor) info.Invoke(this, null);  
14:      },  
15:      request));  
16:  }  

The RawExecute implementation is shown next:

1:  protected T RawExecute<T>(string baseName, IProcessor handler)   
2:    where T : Stream, new() {  
3:    WebOperationContext.Current.OutgoingResponse.ContentType = JSONContentType;  
4:    T stream = new T();  
5:    AssociationsWrapper wrapper = new AssociationsWrapper(baseName);  
6:    try {  
7:      CheckSessionStatus();  
8:      CheckOrganizationActive();  
9:      wrapper.Associations = handler.Process();  
10:    }  
11:    catch (Exception ex) {  
12:      LogFacade.LogFatal(this, "CAS failed", ex);  
13:      wrapper.Success = false;  
14:    }  
15:    finally {  
16:      wrapper.Write(stream);  
17:    }  
18:  return stream;  
19:  }  

There is a part II to this post - that provides more detail of interest.

No comments: