Introduction
Have you ever tried to do self-hosting of WCF services in a moderately sized SOA oriented architecture? If yes, you have surely encountered
a question like this:
The answers to the above questions where either to implement a general service class that implements all service contracts that you want to publish and host it in a single
ServiceHost
, or to use Net TCP Port sharing, by which you give away a great portion of control over the system and also introduce a dependency on the port sharing service,
which is not necessarily a desirable alternative.
The solution to the above problem is to move the routing of requests (that is what the port sharing service does) into your application by utilizing the built-in
RoutingService
class.
Implementation
The architecture of the solution consists of a base service consisting of only
three classes you have to worry about.
The first is an abstract service handle base class that encapsulates a ServiceHost
and takes care of it running on a separate thread.
You can look up the code of this class in the appended code archive.
Next comes the ServiceShell
class, which implements the before-mentioned handle base class and sets up the ServiceHost
to host a RoutingService
at a given base address. It can also expose (read: route to) multiple child services with custom contracts on addresses relative to its base address.
public class ServiceShell : ServiceHandleBase
{
private readonly Uri _baseAddress;
private readonly RoutingConfiguration _routingConfiguration;
private readonly Guid _serviceShellId;
public ServiceShell(Uri baseAddress)
{
if (baseAddress == null)
{
throw new ArgumentNullException("baseAddress");
}
_baseAddress = baseAddress;
_routingConfiguration =
new RoutingConfiguration { RouteOnHeadersOnly = true, SoapProcessingEnabled = true };
_serviceShellId = Guid.NewGuid();
}
protected override ServiceHost CreateServiceHost()
{
var serviceHost = new ServiceHost(typeof(RoutingService), _baseAddress);
serviceHost.Description.Behaviors.Add(new RoutingBehavior(_routingConfiguration));
return serviceHost;
}
protected override ServiceEndpoint CreateService(ServiceHost serviceHost)
{
CheckDisposed();
return serviceHost.AddServiceEndpoint(typeof (IDuplexSessionRouter),
new NetTcpBinding(SecurityMode.None, true), string.Empty);
}
public ServiceHandleBase RegisterChildService(Type serviceImplementation,
Type serviceContract, Uri relativeUri)
{
CheckDisposed();
return new AggregatedService(this, serviceImplementation, serviceContract, relativeUri);
}
private void RemoveFromFilterTable(MessageFilter filter)
{
lock (SyncRoot)
{
if (_routingConfiguration.FilterTable.Remove(filter))
{
UpdateFilterTable();
}
}
}
private void AddToFilterTable(MessageFilter filter,
IEnumerable<ServiceEndpoint> services)
{
lock (SyncRoot)
{
_routingConfiguration.FilterTable.Add(filter, services);
UpdateFilterTable();
}
}
private void UpdateFilterTable()
{
var parentRoutingExtensions = Host.Extensions.Find<routingextension>();
parentRoutingExtensions.ApplyConfiguration(_routingConfiguration);
}
}
So we come to the last class, the AggregatedService
(also a service handle implementation) which is a private inner class of ServiceShell
, since it needs access to some
internal implementation details of ServiceShell
. This class exposes the types passed to it as a service over a named pipes binding.
The named pipe address uses a unique ID of its parent to ensure that multiple instances running on the same system won't have a name collision. When a service of this type
starts, it also registers an EndpointAddressMessageFilter
in the parent ServiceShell
, consequently exposing/routing the service's endpoint
on an address relative to the shell's base address - and of course - over the binding that the shell provides (in our case a net.tcp binding).
private class AggregatedService : ServiceHandleBase
{
private readonly Binding _binding;
private readonly MessageFilter _filter;
private readonly ServiceShell _parent;
private readonly Uri _rootUri;
private readonly Type _serviceContract;
private readonly Type _serviceImplementation;
private readonly Uri _rootUriExternal;
public AggregatedService(ServiceShell parent, Type serviceImplementation,
Type serviceContract, Uri relativeUri)
{
var baseUri = new Uri("net.pipe://localhost/" +
parent._serviceShellId + "/");
_rootUri = new Uri(baseUri, relativeUri);
_parent = parent;
_serviceContract = serviceContract;
_serviceImplementation = serviceImplementation;
_binding = new NetNamedPipeBinding();
_rootUriExternal = new Uri(_parent._baseAddress, relativeUri);
_filter = new EndpointAddressMessageFilter(new EndpointAddress(_rootUriExternal));
}
protected override ServiceHost CreateServiceHost()
{
CheckDisposed();
return new ServiceHost(_serviceImplementation, _rootUri);
}
protected override ServiceEndpoint CreateService(ServiceHost serviceHost)
{
CheckDisposed();
return serviceHost.AddServiceEndpoint(_serviceContract, _binding, string.Empty);
}
public override bool Start()
{
lock (SyncRoot)
{
bool result = base.Start();
if (result)
{
_parent.AddToFilterTable(_filter, new[] { Service });
}
return result;
}
}
}
Demo application
The demo application contains two simple service definitions. A calculation service that contains a two way add operation, and a duplex echo service that does
some string manipulation and returns the result to a duplex callback contract. The services neither use port sharing nor is there a need for a class implementing all service contracts.
static void Main()
{
var shellUri = new Uri("net.tcp://localhost:10011/AggregatedService/");
var shellBinding = new NetTcpBinding(SecurityMode.None, true);
using (var shell = new ServiceShell(shellUri))
using (var echo = shell.RegisterChildService(typeof(EchoServer),
typeof(IEchoServer), new Uri("Echo", UriKind.Relative)))
using (var calc = shell.RegisterChildService(typeof(CalculatorServer),
typeof(ICalculator), new Uri("Calc", UriKind.Relative)))
{
shell.Start();
echo.Start();
calc.Start();
var echoClient = new EchoClient();
var echoChannelFactory = new DuplexChannelFactory<IEchoServer>(echoClient,
shellBinding, new Uri(shellUri, "Echo").ToString());
var echoChannel = echoChannelFactory.CreateChannel();
var calcChannelFactory = new ChannelFactory<icalculator>(shellBinding,
new Uri(shellUri, "Calc").ToString());
var calcChannel = calcChannelFactory.CreateChannel();
var c = calcChannel.Add(1, 2);
echoChannel.Shout("demo");
}
}
Conclusion
As you can see, the RoutingService
virtually eliminates the need to use the external port sharing service, and other unmaintainable approaches.
Points of interest
Instead of the named pipe binding, we could also use the NullTransport binding,
but I did not use it since named pipes are apparently at least as fast.
You can read more about WCF routing on MSDN.
I also stumbled across a nice article about filtering.
The source code of this article is also available on BitBucket as a mercurial repository.