Practical .NET

Ensuring WCF Routing Flexibility

Windows Communication Foundation routing lets you decouple your service consumers and providers to give you the flexibility to modify and extend your services without disrupting your clients.

In designing service-oriented architectures (SOAs), you want to ensure that once consumers start accessing your services, they can always access your service -- despite planned maintenance, server upgrades and enhancements to the services themselves.

At the most basic level, this means that once a service becomes available at a particular endpoint (URL or TCP address), it remains available at that address despite any reorganizations of your datacenter and enhancements to the service. An Enterprise Service Bus (ESB) can be a big help here, but you can also use Windows Communication Foundation (WCF) routing to either address these same concerns or to implement something your ESB doesn't support.

Routing allows you to provide a single, unchanging endpoint for your consumers to use -- an endpoint that your services' many "real" endpoints can hide behind. This allows you to, for instance, relocate your services to a new address without your consumers having to change their endpoints.

Routing also allows you to direct your consumers to a backup service when the primary service is unavailable, either by accident or intent. Routing also enables you to redirect service requests originally targeted to an older version of your service to a new version of your service; again, without any change to your consumers.

WCF 4 supports routing by letting you create a routing host. The routing host provides a single point of contact to your consumers. Routing is rules-based: in the host you build a table of rules that specify how incoming requests are to be redirected. You can update and modify that table over time as services become available and need to be added to the table or are taken offline and need to be removed from the table.

The router processes incoming requests according to those rules, connecting consumers to the appropriate services (and routing outbound messages from the service back to the consumer). WCF routing also supports multicasting -- sending a single service request to multiple services, either for redundancy purposes or to allow simultaneous processing by multiple services (for instance, sending a customer complaint both to the quality-tracking service and the customer-management service).

Routing doesn't support message decomposition or combination. For instance, you can't take an incoming request, break it down into two separate messages and send those messages to different methods on different services. Routing only supports redirecting to matching methods or operations on different services.

Setting up Services
Assume, for instance, that you have an existing service (Customer­Service) that many consumers already access. You've created a new version of the service (EnhancedService) at a new address. The new service includes an auditing feature in many of the service's operations (methods). Of course, to fully exploit the new feature, consumers will have to rewrite their code (there's a new parameter they need to pass to the service's methods).

But in the meantime, even without consumers passing that parameter, you'll get some benefits from the new service if your consumers can be persuaded to use it. Initially, the new service will be left in place at its old address -- because you'd also like to have the ability to switch back to that service if the new one turns out to have problems.

The interfaces for the two services are shown in Listing 1.

Routing redirects the existing service to the new service (and switches back to the old service, if necessary) without disturbing your consumers. Later on, if the consumer authors wish, they can rewrite their programs to supply the new parameter and take full advantage of the new service.

For routing to work, the two services must "look alike," at least to the outside world. My two sample interfaces are in different .NET namespaces (CustomerService and EnhancedService), but that's not a problem: the .NET namespace is private. However, their service namespaces and interface/contract name (ICustomerService) are identical because those are public (that is, they would be included in the Web Services Description Language contract generated from this service). The interface's method names are different (GetCustomerById versus AuditableGetCustomerById). However, by using the Name property on the OperationContract attribute on the AuditableGetCustomerById method, their public names are the same (GetCustomerById). Both methods return a Customer object; that Customer object must be from the same .NET namespace in both versions of the service.

The two methods also accept different parameter lists (one parameter in the original method, two parameters in the new one). In Web Services, though, .NET takes a devil-may-care attitude toward passing parameters, and happily discards extra parameters or provides default values to missing ones. So this isn't a problem, either, provided you include code to handle missing values.

Creating a Router
Given two services that look alike, your next step is to create a service host that handles routing between the two services. In production, your routing host will probably be a Windows Service. The following code would go in the Windows Service OnStart method. For testing purposes, however, it's easier to create a console application; I'll be using this code in the console's Main method.

After adding a console application to the Visual Studio solution with the services that implement the interfaces, you need to add application references to the Microsoft .NET Framework 4 System.S- erviceModel and System.ServiceModel.Routing libraries. At the top of the project's Program.cs file, you'll also need these using directives:

using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Routing;

Next, you'll add the code to the Program.cs file's Main method to instantiate the ServiceHost using the .NET-provided RoutingService class. The foreach loop in the code finds the ServiceDebugBehavior object that's automatically added to your ServiceHost Behaviors collection, and sets it to return lots of detail in any error messages. While you need this for debugging during development, you'll want to remove it when you move this code to the Windows Service you'll use in production:

ServiceHost host = new ServiceHost(typeof(RoutingService));
foreach (var sb in host.Description.Behaviors)
{
  ServiceDebugBehavior sdb = sb as ServiceDebugBehavior;
  if (sdb != null)
  {
    sdb.IncludeExceptionDetailInFaults = true;
  }
}

Your next step is to create ServiceEndpoint objects for each of the services involved in the process. In this example, there are three endpoints: the service host (the router) and the two services to which it routes requests (CustomerService and EnhancedService). All three ServiceEndpoint objects need a contract that's based on the .NET-provided IRequestReplyRouter interface, so a good next step is to create a ContractDescription object you can use when creating the ServiceEndpoints:

ContractDescription cdRouter = ContractDescription.GetContract(
  typeof(IRequestReplyRouter));

When creating a ServiceEndpoint object, you pass the ContractDescription you just created, the type of binding you want to use with your service and an EndpointAddress object holding the address for the service.

For the router's ServiceEndpoint (the place where consumers will now send their messages), you can use any endpoint you wish. For the endpoint of the services to which you're routing, you'll need to use the endpoints they've assigned themselves in their .config files (or that ASP.NET has assigned them, if your services are part of an ASP.NET project).

The following code creates the ServiceEndpoint for the router (at port 9000) and the endpoint for one of the services being routed to (at port 8000). The code for the other service's ServiceEndpoint would be almost identical, with only the URL changing:

ServiceEndpoint se = new ServiceEndpoint(cdRouter,
  new WSHttpBinding(),
  new EndpointAddress(
    "http://localhost:9000/CustomerService/"));

ServiceEndpoint MainService = new ServiceEndpoint(cdRouter,
  new WSHttpBinding(),
  new EndpointAddress(
    "http://localhost:8000/MainService/CustomerService/"));

The router's ServiceEndpoint gets special treatment -- it's added to the host's ServiceEndpoint collection using the AddService­Endpoint method:

host.AddServiceEndpoint(se);

Defining Routing Rules
The next step is to create a MessageFilterTable object that holds the rules for redirecting requests made to the routing service to the service the consumer actually wants. The MessageFilterTable holds an IEnumerable collection of ServiceEndpoints.

Once the table is created, you add it to a RoutingConfiguration object, pass the RoutingConfiguration object to a RoutingBehavior object and add the RoutingBehavior object to the host's Behaviors collection:

MessageFilterTable <IEnumerable <ServiceEndpoint > > mft = 
  new MessageFilterTable <IEnumerable <ServiceEndpoint > >();
RoutingConfiguration rc = new RoutingConfiguration(mft, false);
RoutingBehavior rb = new RoutingBehavior(rc);
host.Description.Behaviors.Add(rb);

When adding the table to the RoutingConfiguration, you can use the second parameter to specify whether routing rules apply only to the request's header. This code specifies false, which allows me to route messages based on the content of the message (for example, the parameters being passed to the service's methods).

With the table created and made available to the router, you can populate the table with objects that inherit from the MessageFilter class. Each variation on the MessageFilter class specifies criteria for directing a request, and is added to the table with a collection of ServiceEndpoints. While the collection of ServiceEndpoints associated with a filter will typically have only one entry, the ability to add multiple endpoints lets you implement multicasting: one request sent to multiple services.

The simplest MessageFilter is the MatchAllMessageFilter, which redirects all requests to its endpoint. This code creates a List of ServiceEndpoints and adds the ServiceEndpoint created earlier to the list. The code then creates a MatchAllMessageFilter and adds it, with the List of ServiceEndpoints, to the MessageFilterTable:

List <ServiceEndpoint > primaryEndPoints = new List <ServiceEndpoint >();
primaryEndPoints.Add(MainService);
MessageFilter mamf = new MatchAllMessageFilter();
mft.Add(mamf, primaryEndPoints);

When adding a MessageFilter to the table, you can specify a priority as the third parameter to the Add method. When a matching rule is found, no other rules at a lower priority will be examined. When you create the MessageFilterTable, you can specify the default priority for MessageFilters added without a priority (if you don't provide the value, the default priority is 0).

Exercising Your Router
The final step in your router is to open your service host when you start up and close it when you're done. In a Windows Service, you'd put the Open method call in the Windows Service OnStart method and the Close method call in the OnStop method.

Because I'm using a console application here, I put in a WriteLine to notify me that the router's ready and a ReadLine to hold my application in memory between the Open and Close:

host.Open();
Console.WriteLine("Router up");
Console.ReadLine();
host.Close();
Exercising the Rules

To test your router, you'll need to create a client program to call operations on your service. You can use any project type you want (I used a Windows Form), but you'll need to add a Service Reference to the client project that points to one of the services your router works with (this will generate the proxy that your client needs).

After adding the Service Reference, go into your client's .config file and change the endpoint address from the service's endpoint to the endpoint you assigned to your router (http://localhost:9000/CustomerService, in my example).

Before you can test your application, you'll need to configure your startup projects. Right-click on your solution's name in Solution Explorer and select Set StartUp Projects. In the resulting dialog, select the Multiple Startup Projects choice and set the Action for your routing host and client projects to Start. Use the up and down buttons to put your routing host first in the list.

Unfortunately, unless you're running under an Administrator account, when you first test your application you'll probably get an error message indicating that you don't have the necessary security to access the service.

The error message is actually pretty good: it tells you to use the netsh command-line utility, passing the specified endpoint and your login identity (domainname\username). To do that from your Start menu, right-click on the Command Prompt and select Run as Administrator. When the window opens, enter the netsh command as shown in the error message, inserting your domainname/username (if you're not sure who you are, use the whoami command to display your current identity). Everything should work the next time you test your application.

At this point the router is sending requests for the original service to the original service -- not much value added. The next step is to replace the CustomerService ServiceEndpoint added to the table with the EnhancedService ServiceEndpoint to which you want to redirect requests. You can do that without having to shut down the host by updating the list of service endpoints associated with the MessageFilter. This code, for instance, removes the ServiceEndpoint named MainService from the list of services used with my message filter and adds the ServiceEndpoint held in EnhancedService:

primaryEndPoints.Remove(MainService);
primaryEndPoints.Add(BackupService);

More Rules
While I've used the MatchAllMessageFilter here, there are about a half-dozen more MessageFilters included in the .NET Framework 4. For instance, the ActionMessageFilter lets you redirect requests for specific methods or operations to a different service. This example would let me redirect requests to the GetCustomerById method to some other service with an identical method name:

primaryEndPoints.Add(MainService);
ActionMessageFilter amf = new ActionMessageFilter(
  "http://www.phvis.com/customermanagement/ICustomerService/GetCustomerById");
mft.Add(amf, primaryEndPoints);

If none of the included MessageFilters meet your needs, you can build your own custom MessageFilter on top of the base MessageFilter class.

But the real issue is when you should do this: You should create the router as part of writing your service, so that you can pass your router's endpoint rather than the service's endpoint to consumers (though, of course, moving an existing service to a new address and assigning the router the service's old address wouldn't be too awkward to implement).

Once consumers are accessing your services through your router, you can do whatever you need to behind the router. A little bit of preventative maintenance at the start will give you lots of flexibility at the end.

comments powered by Disqus

Featured

Subscribe on YouTube