- Download the sample source code for this post here
In this post, I will attempt to describe what I learned while working out how to separate the concerns of a WCF service layer from those of a presentation layer and vice-versa while using a redistributable client assembly and NOT using client proxy code generated by Visual Studio 'Add Service Reference' or svcutil.exe.
The code and scenarios do not necessarily represent best practices and will most likely offend some purists but I do believe that the topics covered represent real world challenges and may provide some insight into some not-so-obvious aspects of SOA with WCF.
The Problem Domain
On the service side, your domain objects may be derived from a common base class used by the DAL and may also be decorated with attributes that belong to your DAL. It is probable that you would like to hide these implementation details AND the assembly dependencies from the client for any number of reasons.
An example would be an ActiveRecord DAL, which is attribute based and carries dependencies on several Castle Project and NHibernate assemblies.
There may also be members of the domain objects themselves that are not relevant to the client and surfacing them on the client represents noise or unnecessary interface dependencies.
Conversely, the client application may require domain object characteristics that are not relevant to the service and present unnecessary dependencies and code maintenance. The typical INotifyPropertyChanged implementation required for client side binding is a prime example.
Additionally, your service may expose methods that are not relevant to the specific target client and/or may be subject to change. There is also the issue of WSDL/MEX visibility which may not be possible or allowed.
With a bit of imagination, you could loosely correlate these concerns to the SOLID principles of Interface Segregation , Liskov Substitution , as well as the principles of Information or Implementation Hiding and Separation of Concerns .
When building WCF services and clients using Visual Studio, observing the intent of these principles is not always an obvious task but I have found that they can be addressed, to some degree, using a few simple strategies.
Common Sub-Optimal Scenarios
The general nature of the typical WCF construction workflows in a Visual Studio development process do not result in code that addresses the issues and challenges listed thus far.
- Add Service Reference / svcutil.exe client proxy code generation:
The service contract and data contracts are proxied, in total, in generic tool generate code. While serviceable, it is a one-size-fits all approach and that is the kind of code it produces.
- Client proxy code generation using a shared domain library:
Using service proxy code generation with a shared library ostensibly promotes code reuse but introduces dependency leakage, exposes implementation details and increases code complexity.
Pictured is the target scenario, admittedly contrived, that illustrates the challenges presented.
Viewing this class diagram, click for full size image, three differences can be observed:
- The names of corresponding types are not semantically similar.
- There are fewer service methods and data members present on the client types.
- The base classes of the domain objects are neither semantically nor functionally similar.
The first issue of identifier mismatch can be resolved by the use of the Namespace and Name members of the ServiceContract and DataContract attributes. This applies to type names as well as method and member names.
By decorating each corresponding type and interface of each assembly with
DataContract attributes instantiated with similar
Name arguments, as shown in Listing 1, the identifier mismatch can be correctly handled by the serialization process.
While it is entirely possible to have two assemblies with semantically similar namespaces and type names, which would serve to satisfy the XML serializer, the mismatch is deliberately introduced to demonstrate the use of
DataContract members mentioned previously.
The second issue, missing members on the client side, is implicitly handled by WCF, provided that the difference is reductive and that the signatures of the members that are present on the subset are semantically similar.
This could represent an application of the Interface Separation and Liskov substitution principals by breaking a large monolithic service implementation into smaller, more focused, logical interfaces, none of which the service need be aware of as well as hiding irrelevant or protected fields on domain objects.
The example shown simply seeks to hide a specific method,
AMethodNotExposedToClient, from the client interface and reduce the number of members surfaced on the domain objects.
While it is possible to simply omit the
DataMember attribute on specific members of the service side domain objects, the scenario presented assumes that the service side
DataContracts are either not open to modification or that the data managed by those members is relevant to the service or must be serialized for consumption elsewhere but should not be surfaced on this particular client. In the example, this is shown by the omission of
Category.Picture and various members on the
Product class. This smells like Information Hiding to me.
The fact that data members are omitted from the client-side contracts, as in the example given, does not affect the data that is serialized and pushed over the wire. It only affects the shapes of the deserialized objects.
If round-tripping of the domain objects is required and the omitted fields are relevant to the service side processes, the IExtensibleDataObject interface can be implemented on the client classes to maintain round-trip data fidelity.
IExtensibleDataObject describes to the
XmlSerializer a structured member in which to store those service type members for which there are no corresponding client side members.
The use of
IExtensibleDataObject is also a strategy for future proofing deployed libraries against additions to the domain objects.
The third, and possibly most salient, issue presented is the difference in inheritance hierarchies between the service and client domain objects which could be viewed as a Separation of Concerns.
Whether you are an NHibernate purist implementing very strict POCO DTO objects with mapping files or a pragmatic ALT.Netter who uses whatever makes sense for the situation and has landed on
SubSonic DAL with custom attributes coming out of your ears and ‘polluting’ your domain objects, you probably do not want to (further) ‘pollute’ your domain objects with client side implementation details and dependencies, such as
INotifyProperyChanged, that can, and will, change over the lifetime of a client application or applications.
Nor do you, if using
ActiveRecord for example, wish to expose the client to your DAL implementation details and introduce assembly deployment dependencies that have to be maintained.
These concerns preclude the use of a single assembly shared by the client and the service. Fortunately, the primary purpose of the
DataContract attributes are to enable the use of arbitrary assemblies and types to fulfill the contracts they describe.
But this only takes us half the way there. Even with the semantic differences mapped with metadata, svcutil.exe and/or Visual Studio’s ‘Add Service Reference’ tool, when pointed at the service from a client with a reference to the client redistributable, will choke when importing the WSDL from the service when it tries to apply the WSDL using the semantically similar contracts presented in the client redistributable assembly because the domain objects have differing base classes.
But all is not lost. We do have a semantically similar service contract,
IService1ClientPerspective, present in the client assembly which references the client version of the domain objects.
Using this interface and the
ChannelFactory class, we can spin up a channel, in just a few lines of code, which will happily map between the server and client contracts as well as maintaining data fidelity via
using (var cf = new using ( var cf =
cf.Credentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;
using (IService1ClientPerspective svcClient = cf.CreateChannel())
Given the target scenario, this is the only viable option but even if that was not the case, it is my opinion that spinning up a channel yourself presents a more robust and controllable service invocation strategy compared to depending on 10+ files filled with metadata and realms of generated code.
<plug> I have nothing against code generation. In fact I would suggest use of a targeted DTO code generator to produce your domain objects. For this article, I use
DeadSimpleDTO, a DTO code generator I wrote, as an exercise, that is implemented as a single SQL batch script coupled with a T4 template harness to provide automatic generation and synchronization of the service and client domain models. As you can tell, I just hijacked a couple tables from Northwind. </plug>
Batteries Included, No Assembly Required
The sample solution contains a full implementation of the solution presented, however contrived it may be.
Simply set the client application as startup project and F5 or Shift-F5 and the service library should be spun up in the WCF Test Host for consumption by the client application.
You will find the generated DTO classes as child objects of the Northwind.Excerpt.ClientProfile.txt and Northwind.Excerpt.ServerProfile.txt files.
If you wish to try out
DeadSimpleDTO and regenerate the classes, ensure the Northwind connection string is in app.config and change the extension from .txt to .tt and right-click on each, in turn, in Solution Explorer and select ‘Run Custom Tool’.
Clicking the ‘Transform All Text Templates’ button will probably not produce desired results as the templates rely on the current active project to find files.
Also, it seems that initializing the text templating environment seems to lock up VS for a few moments. This apparently is normal.
For more information on
DeadSimpleDTO, see http://deadsimpledto.codeplex.com.
Comments, suggestions, corrections and complaints are all welcome in the comments or by email.