Architectural Overview: Creating Streamlined, Simplified, yet Scalable WCF Connectivity
Contents
- Introduction
- Service Structure
- Service MetaData
- Service Implementation
- Service Client Without Magic
- Conclusion
Introduction
One of the most awesome things about WCF is that the concepts scale extremely well. If you understand the ABCs of WCF, then you can do anything from creating a simple Hello World to a complex sales processing service. It's all based on having an address, a binding, and a contract. All the other concepts like behaviors, validators, and service factories are simply supplemental to the core of the system. When you understand the basics, you have the general essence of all of WCF.
Because of this fully-scalable ABC concept, I'm able to use the same pattern for WCF architecture and development for every solution. This is really nice because it makes it so that I don't have to wait time designing a new setup every time a new problem comes along. In this discussion, I would like to demonstrate how you can create your own extremely efficient WCF solution based on my template. Along the way, you will also learn a few pieces of WCF internals to help you understand WCF better.
Before I begin the explanation though, keep in mind that most concepts mentioned here are demonstrated in my Minima Blog Engine 3.1. This is my training software demonstrating an enormous world of modern technologies. It's regularly refactored and often fully re-architected to be in line with newer technologies. This blog engine relies heavily on WCF as it's a service-oriented blog engine. Regardless of how many blogs you have (or any other series of "content entries"-- for example, the documentation section of the Themelia web site is Minima), you have a single set of services that your entire organization uses. If you understand the WCF usage in Minima, you will understand WCF very well.
Now, onto the meat (or salad, for the vegetarians) of the discussion...
Service Structure
On any one of my solutions, you will find a X.Service project and a X.ServiceImpl project where X is the solution "code" (either the solution name or some other word that represents the essence of the solution). The former is the public .NET project which contains service contracts, data contracts, service configuration, and service clients. The latter is the private .NET project which contains the service implementations, behaviors, fault management, service hosts, validators, and other service-side-only, black-boxes portions of the service. All projects have access to the former, only the service itself will ever even know about the latter.
This is a very simple setup based upon a public/private model, like in public key cryptography. The idea is that everything private is protected with all your might and everything public is released for anyone, within the context of the solution, to see.
For example, below is the Minima.Service project for Minima Blog Engine 3.1. Feel free to ignore the folder structure, no one cares about that. Just because I make each folder a namespace with prefixed folder names for exclusions, doesn't mean anyone else in the world does. I find it to be the most optimal way to manage namespaced and non-namespaced groups, but the point of this discussion is the separation of concerns in the projects.
For the time being, simply notice that data contracts and service contracts are considered public. Everything else in the file is either meaningless to this discussion or will be discussed later.
Here is the Minima.ServiceImpl project for the same solution:
Here you can see anything from the host factory to various behaviors to validators to fault management to LINQ-to-SQL capabilities. Everything here is considered private. The outside world doesn't need to know, therefore shouldn't know.
For the sake of a more simplified discussion, let's switch from the full-scale solution example of Minima to a smaller "Person" service example. This "Person" service is part of the overall "Contact" solution. Here's the Contact.Service for our Contact solution:
As you can see, you have a standard data contract, a service contract, and a few other things, which will be discussed in a bit.
For this example, we don't need validators, fault management, or behaviors, or host factories, all we need is a simple service in our Contact.ServiceImpl project:
By utilizing this model separating the private from the public you can easily send the Contact.Service assembly to anyone you want without requiring them to create their own client proxy or use the painfully horrible code generated by "Add Service Reference", which ranks in my lists as one of the worst code generators right next to FrontPage 95 and Word 2000.
As a side note, I should mention that I have colleagues who actually take this a step further and make a X.Service.Client project which houses the WCF client classes. They are basically following a Service/Client/Implementation model where as I'm following a Public/Private model. Just use which ever model makes sense for you.
The last piece needed in this WCF setup is the host itself. This is just a matter of creating a new folder for the web site root, adding a web.config, and adding a X.svc file. Period. This is the entire service web site.
In the Person service example, the service host has only two files: web.config and Person.svc.
Below is the web.config, which declares two endpoints for the same service. One of the endpoints is a plain old fashioned ASMX style "basic profile" endpoint and the other is one to be used for JSON connectivity.
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.serviceModel> <behaviors> <endpointBehaviors> <behavior name="JsonEndpointBehavior"> <enableWebScript /> </behavior> </endpointBehaviors> </behaviors> <services> <service name="Contact.Service.PersonService"> <endpoint address="json" binding="webHttpBinding" contract="Contact.Service.IPersonService" behaviorConfiguration="JsonEndpointBehavior" /> <endpoint address="" binding="basicHttpBinding" contract="Contact.Service.IPersonService" /> </service> </services> </system.serviceModel> </configuration>
The Person.svc is even most basic:
<%@ ServiceHost Service="Person.Service.PersonService" %>
This type of solution scales to any level. If you want to add a service, just add a new X.svc file and register as a new service. If you want to add another endpoint, just add that line. It's incredibly simple and scales to solutions of any size and even works well with non-HTTP services like netTcpBinding services.
Service MetaData
Now let's look at each of these files to see how they are optimized. First, lets' look at the data contract, Person:
using System; using System.Runtime.Serialization; //+ namespace Contact.Service { [DataContract(Namespace = Information.Namespace.Contact)] public class Person { //- @Guid -// [DataMember] public String Guid { get; set; } //- @FirstName -// [DataMember] public String FirstName { get; set; } //- @LastName -// [DataMember] public String LastName { get; set; } //- @City -// [DataMember] public String City { get; set; } //- @State -// [DataMember] public String State { get; set; } //- @PostalCode -// [DataMember] public String PostalCode { get; set; } } }
Everything about this file should be self explanatory. There's a data contract attribute on the class and data member attributes on each member. Simple. But what's with the data contract namespace?
Well, earlier this year, a co-architect of mine mentioned to me that you can centralize your namespaces in static locations. Genius. No more typing the same namespace on each and every data and service contract. Thus, the following file is included in each of my projects:
using System; //+ namespace Contact.Service { public class Information { //- @NamespaceRoot -// public const String NamespaceRoot = "http://www.netfxharmonics.com/service/"; //+ //- @Namespace -// public class Namespace { public const String Contact = Information.NamespaceRoot + "Contact/2008/11/"; } } }
If there are multiple services in a project, and in 95%+ of the situations there will be, then you can simply add more services to the Namespace class and reference them from your data and service contracts. Thus, you never, EVER have to update your service namespaces in more than one location. You can see this in the service contract as well:
using System; using System.ServiceModel; //+ namespace Contact.Service { [ServiceContract(Namespace = Information.Namespace.Contact)] public interface IPersonService { //- GetPersonData -// [OperationContract] Person GetPersonData(String personGuid); } }
This service contract doesn't get much simpler. There's no reason to discuss it any longer.
Service Implementation
For the sake of our discussion, I'm not going to talk very much at all about the service implementation. If you want to see a hardcore implementation, go look at my Minima Blog Engine 3.1. You will see validators, fault management, operation behaviors, message headers, and on and on. You will seriously learn a lot from the Minima project.
For our discussion, here's our Person service:
using System; //+ namespace Contact.Service { public class PersonService : Contact.Service.IPersonService { //- @GetPersonData -// public Person GetPersonData(String personGuid) { return new Person { FirstName = "John", LastName = "Doe", City = "Unknown", Guid = personGuid, PostalCode = "66062", State = "KS" }; } } }
Not too excited, eh? But as it is, this is all that's required to create a WCF service implementation. Just create an every day ol' class and implement a service contract.
As I've mentioned, though, you could do a ton more in your private service implementation. To give you a little idea, say you wanted to make absolutely sure that no one turned off metadata exchange for your service. This is something that I do for various projects and it's incredibly straight-forward: just create a service host factory, which creates the service host and programmatically adds endpoints and modified behaviors. Given that WCF is an incredibly streamlined system, I'm able to add other endpoints or other behaviors in the exact same way.
Here's what I mean:
using System; using System.ServiceModel; using System.ServiceModel.Description; //+ namespace Contact.Service.Activation { public class PersonServiceHostFactory : System.ServiceModel.Activation.ServiceHostFactory { //- @CreateServiceHost -// protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { ServiceHost host = new ServiceHost(typeof(PersonService), baseAddresses); //+ add metadata exchange ServiceMetadataBehavior serviceMetadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>(); if (serviceMetadataBehavior == null) { serviceMetadataBehavior = new ServiceMetadataBehavior(); host.Description.Behaviors.Add(serviceMetadataBehavior); } serviceMetadataBehavior.HttpGetEnabled = true; ServiceEndpoint serviceEndpoint = host.Description.Endpoints.Find(typeof(IMetadataExchange)); if (serviceEndpoint == null) { host.AddServiceEndpoint(typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), "mex"); } //+ return host; } } }
Then, just modify your service host line:
<%@ ServiceHost Service="Person.Service.PersonService" Factory="Person.Service.PersonServiceFactory" %>
My point in mentioning that is to demonstrate how well your service will scale based upon a properly setup base infrastructure. Each thing that you add will require a linear amount of work. This isn't like WSE where you need three doctorates in order to modify the slightest thing.
Service Client without Magic
At this point, a few of my colleagues end their X.Service project and begin a X.Service.Client class. That's fine. Keeping the client away from the service metadata is nice, but its not required. So, for the sake of this discussion, let's continue with the public/private model instead of the service/client/implementation model.
Thankfully, I have absolutely no colleagues who would ever hit "Add Service Reference". I've actually never worked with a person who does this either. This is good news because, as previously mentioned, the code generated by the add service reference is a complete disaster. You also can't modify it directly without risking your changing being overwritten. Even then, the code is SO bad, you wouldn't want to edit it. There's no reason to have hundreds or THOUSANDS of lines of code to create a client to your service.
A much simpler and much more manageable solution is to keep your code centralized and DRY (don't repeat yourself). To do this, you just need to realize that the publicly accessible X.Service project already contains the vast majority of everything needed to create a WCF client. Because of this, you may access the service using WCF's built in mechanisms. You don't need anything fancy, it's all built right in.
Remember, WCF simply requires the ABC: and address, binding, and contract. On the client-side, WCF uses this information to create a channel. This channel implements your service contract, thus allowing you to directly access your WCF service without any extra work required. You already have the contract, so you just have to do is declare the address and binding. Here's what I mean:
//+ address EndpointAddress endpointAddress = new EndpointAddress("http://localhost:1003/Person.svc"); //+ binding BasicHttpBinding basicHttpBinding = new BasicHttpBinding(); //+ contract IPersonService personService = ChannelFactory<IPersonService>.CreateChannel(basicHttpBinding, endpointAddress); //+ just use it! Person person = personService.GetPersonData("F488D20B-FC27-4631-9FB9-83AF616AB5A6"); String firstName = person.FirstName;
No 3,000 line client generated code, no excess classes, no configuration. Just direct access to your side.
Of course, if you want to use a configuration file, that's great too. All you need to do in create a system.serviceModel section in your project or web site and declare your service endpoint. What's really nice about this in WCF is that, since the concept for WCF are ABC on both the client and server, most of the configuration is already done for you. You can just copy and paste the endpoint from the service configuration to your client configuration and add a name element.
<system.serviceModel> <client> <endpoint name="PersonServiceBasicHttpBinding" address="http://localhost:1003/Person.svc" binding="basicHttpBinding" contract="Simple.Service.IPersonService" /> </client> </system.serviceModel>
For the most part, you will also need to keep any bindings on the service side as well, though you wouldn't copy over validation information.
At this point you can just change the previously used WCF channel code to use an endpoint, but that's an architectural disaster. You never want to compile configuration information into your system. Instead of tying them directly, I like to create a custom configuration for my solution to allow the endpoint configuration to change. For the sake of this discussion though, let's just use appSettings (do NOT rely on appSettings for everything! That's a cop-out. Create a custom configuration section!). Here's our appSetting:
<appSettings> <add key="PersonServiceActiveEndpoint" value="PersonServiceBasicHttpBinding" /> </appSettings>
At this point, I can explain that Configuration class found in the public Contact.Service project:
using System; //+ namespace Contact.Service { public static class Configuration { //- @ActivePersonServiceEndpoint -// public static String ActivePersonServiceEndpoint { get { return System.Configuration.ConfigurationManager.AppSettings["PersonServiceActiveEndpoint"] ?? String.Empty; } } } }
As you can see, this class just gives me strongly-typed access to my endpoint name. Thus allowing me to access my WCF endpoint via a loosely connected configuration:
//+ configuration and contract IPersonService personService = new ChannelFactory<IPersonService>(ServiceConfiguration.ActivePersonServiceEndpoint).CreateChannel(); //+ just use it! Person person = personService.GetPersonData("F488D20B-FC27-4631-9FB9-83AF616AB5A6"); String firstName = person.FirstName;
When I feel like using a different endpoint, I don't modify the client endpoint, but just add another one with a new name and update the appSettings pointer (if you're info fancy names, this is essentially the bridge pattern).
Now, while this is a great way to directly access data by people who understand WCF architecture, it's probably not a good idea to allow your developers to have direct access to system internals. Entry-level developers need to focus on the core competency of your company (i.e. sales, marketing, data management, etc), not ponder the awesomeness of system internals. Thus, to allow other developers to work on a solution without having to remember WCF internals, I normally create two layers of abstraction on top of what I've already shown.
For the first layer, I create a concrete ClientBase class to hide the WCF channel mechanics. This is the type of class that "Add Service Reference" would have created if your newbie developers accidentally used it. However, the one we will create won't have meaningless attributes and virtually manageable excess code.
Below is our entire client class:
using System; using System.ServiceModel; using System.ServiceModel.Channels; //+ namespace Contact.Service { public class PersonClient : System.ServiceModel.ClientBase<IPersonService>, IPersonService { //- @Ctor-// public PersonClient(String endpointConfigurationName) : base(endpointConfigurationName) { } //+ //- @GetPersonData -// public Person GetPersonData(String personGuid) { return Channel.GetPersonData(personGuid); } } }
The pattern here is incredibly simple: create a class which inherits from System.ServiceModel.ClientBase<IServiceContractName> and implements your service contract. When you implement the class, the only implementation you need to add is a call to the base channel. In essence, all this class does is accept calls and pass them off to a pre-created channel. When you add a new operation to your service contract, just implement the interface and add a single line of connecting code to wire up the client class.
The channel creation mechanics that I demonstrated earlier is now done provided automatically by the ClientBase class. You can also modify this class a little by bridging up to a total of 10 different constructors provided by ClientBase. For example, the following constructor call will allow developers to specify a specific binding and endpoint:
public PersonClient(Binding binding, EndpointAddress address) : base(binding, address) { }
At this point, we have something that protects developers from having to remember how to create a channel. However, they still have to mess with configuration names and must remember to dispose this client object (there's an open channel, remember). Therefore, I normally add another layer of abstraction. This one will be directly accessibly for developer user.
This layer consists of a series of service agents. Each service has its own agent and is essentially a series of static methods which provide the most efficient means of making a service call. Here's what I mean:
using System; //+ namespace Contact.Service { public static class PersonAgent { //- @GetPersonData -// public static Person GetPersonData(String personGuid) { using (PersonClient client = new PersonClient(ServiceConfiguration.ActivePersonServiceEndpoint)) { return client.GetPersonData(personGuid); } } } }
As you can see, the pre-configured service endpoint is automatically used and the PersonClient is automatically disposed at the end of the call (ClientBase<T> implements IDisposable). If you want to use a different service endpoint, then just change it in your appSettings configuration.
Conclusion
At this point, I've explained every class or each project in my WCF service project model. It's up to you to decide how to best create and manage your data and service contracts as well as clients. But, if you want a streamlined, efficient, model for all your service projects, you will want to create a publicly accessible project to house all your reusable elements.
Also, remember you don't need a full-on client class to access a service. WCF communicates with channels and channel creation simply requires an address, binding, and contract. If you have that information, just create your channel and make your call. You can abstract the internals of this by using a ClientBase object, but this is entirely optional. If the project you are working on requires hardcore WCF knowledge, there's no reason to pretty it up. However, if non-WCF experts will be working with your system, abstractions are easy to create.
Links
- Sample Solution for this Discussion
- Minima Blog Engine 3.1 Training Software
- Network Security Access Restrictions in Silverlight 2
- AutoResetEvent on MSDN
- Architectural Overview: Using LINQ in WCF
Love Sudoku? Love competition? Try the new Sudokian.com experience today.