I’ve previously posted an article where I provide a solution for dealing with the limitation of hosting a WCF Service on an IIS site with multiple site bindings. Unfortunately such solution requires manually changing the configuration file and is not really portable, so needs to be kept in synch with any changes made to the site bindings themselves.
So I recently had an idea for a smarter implementation of a similar idea that would be much more portable and require no manual intervention when applied to different deployment environments. This implementation is also based on a custom ServiceHostFactory but doesn’t require any changes to your configuration file, instead it will figure out the adjustments and fix-up steps that need to be made do the service’s endpoint at runtime, based on the deployment environment so you don’t have to do that. Based on my previous post, it’s clear that the problem with having multiple site bindings is that WCF doesn’t know how to create physical absolute addresses for endpoints that have relative addresses. That’s not a very hard problem to solve, all we need to do is make sure that all endpoints have reasonable absolute addresses and we’re good to go. But the WCF hooks that are available to us to inject that code, are called too late in the game when all addresses have already been converted to absolute addresses or, a failure to do so (the notorious “This collection already contains an address with scheme http...") has already occurred.
The basic approach that I came up with is to make utilitarian calls to CreateServiceHost with some specially massaged inputs such that I can discover the form of the original address by inspecting the results. I then dispose of the ServiceHost (which I never open) and go ahead and build the real one and then I replace all the endpoints with ones that have the addresses I computed base on the information I gathered. Here’s some more detail on how this:
When the CreateServiceHost is invoked by the WCF runtime, the ServiceHostFactory will inspect the list of base addresses it gets passed. It will then go through the list and detect if any Uri scheme has more than a single base address in the list. If none are found, we have nothing to do and can call the base implementation. If, however, it finds one it will create a dummy address for that scheme and replace any additional base addresses with that dummy address, otherwise it will just keep the base address as-is. So, if you had the following 4 base addresses:
http://foo.com:8080/
http://localhost/
http://10.0.0.1:8181/
net.tcp://localhost/
this will result in the following list of 2 base addresses, a real one and a dummy one:
http://i-am-a-dummy-host/
net.tcp://localhost/
The code for this looks more or less like this:
// count base addresses per scheme and create a filtered collection where we skip all but the 1st address for each scheme Dictionary<string, List<Uri>> allBaseAddresses = new Dictionary<string, List<Uri>>(); Dictionary<string, Uri> dummyBaseAddresses = new Dictionary<string, Uri>(); List<Uri> filteredBaseAddresses = new List<Uri>(); bool filtered = false; foreach (Uri address in baseAddresses) { if (!allBaseAddresses.ContainsKey(address.Scheme)) { filteredBaseAddresses.Add(address); dummyBaseAddresses.Add(address.Scheme, address); allBaseAddresses.Add(address.Scheme, new List<Uri>()); } else { // an address for this scheme was already added, that means we're going to filter them out UriBuilder ub = new UriBuilder(address.Scheme, MbaServiceHostFactory.DummyHost); dummyBaseAddresses[address.Scheme] = ub.Uri; filtered = true; } allBaseAddresses[address.Scheme].Add(address); } if (!filtered) { // if no filtering occured we don't need to perform fixups, so call the base implementation return base.CreateServiceHost(constructorString, baseAddresses); }
With the resulting collection, we now call the base CreateServiceHost implementation with the sole purpose of building the ServiceDescription which contains a collection of ServiceEndpoint instances. The code for this is trivial, it looks more or less like this:
// if filtering occured, we need to multiplex some of the endpoints, to find out which ones // we create a temporary ServiceHost and feed it the dummy base addresses we just computed ServiceHostBase dummyService = base.CreateServiceHost(constructorString, dummyBaseAddresses.Values.ToArray());
We now go through the list of ServiceEndpoint and whenever we hit an endpoint that has an address containing the dummy address, we are sure that this endpoint had a relative address in configuration so we remove this endpoint, reverse engineer its original relative address and replace it with a collection of endpoints that are all equal, except for their addresses. Such addresses are the result of combining the relative address with all the base addresses (which we remember from our initial inspection) that have a matching scheme. So, if we found 2 endpoints with the following addresses:
http://i-am-a-dummy-host/Service1.svc
http://i-am-a-dummy-host/Service1.svc/mex
we’d replace them with the following 6:
http://foo.com:8080/Service1.svc
http://localhost/Service1.svc
http://10.0.0.1:8181/Service1.svc
http://foo.com:8080/Service1.svc/mex
http://localhost/Service1.svc/mex
http://10.0.0.1:8181/Service1.svc/mex
All other endpoints remain as-is, and since at this point all endpoints have real absolute addresses, we’re ready for Open() and can just return this ServiceHost to WCF. The code for this is a little complex, I’ve decided to break it down in two phases, one where I just divide these endpoints in the two classes I’ve just described:
// now we simply go over the endpoints and figure out which ones need to be multiplexed by // checking if they were bound to any of the dummy base addresses List<ServiceEndpoint> singleEndpoints = new List<ServiceEndpoint>(); List<ServiceEndpoint> multiplexEndpoints = new List<ServiceEndpoint>(); foreach (ServiceEndpoint endpoint in dummyService.Description.Endpoints) { List<Uri> bas = allBaseAddresses[endpoint.Binding.Scheme]; if (bas != null && bas.Count > 1) { if (endpoint.ListenUri != null) { if (endpoint.ListenUri.Host == MbaServiceHostFactory.DummyHost) { multiplexEndpoints.Add(endpoint); continue; } } else if (endpoint.Address != null && endpoint.Address.Uri.Host == MbaServiceHostFactory.DummyHost) { multiplexEndpoints.Add(endpoint); continue; } } singleEndpoints.Add(endpoint); }
The second phase, is preceded by the creation of the actual ServiceHost and clearing of the endpoints from its Description, this is trivial:
// now we can create the real ServiceHost, but we'll use the // filteredBaseAddresses to guarantee exactly one address per scheme ServiceHostBase service = base.CreateServiceHost(constructorString, filteredBaseAddresses.ToArray());
// for simplicity we'll completely rewrite the endpoint collection // so we don't have to break enumeration everytime we change the collection service.Description.Endpoints.Clear();
We then rebuild the collection of endpoints from scratch by simply adding the ones that required no change:
// add the endpoints that need no modification foreach (ServiceEndpoint endpoint in singleEndpoints) { service.Description.Endpoints.Add(endpoint); }
And now, for each of the ones in the other set, we create multiple endpoints for each base addresses we had found with a matching scheme:
// multiplex the endpoints that correspond to multiple base addresses foreach (ServiceEndpoint endpoint in multiplexEndpoints) { List<Uri> bas = allBaseAddresses[endpoint.Binding.Scheme]; int count = 0; foreach (Uri ba in bas) { ServiceEndpoint copy = new ServiceEndpoint(endpoint.Contract) { Address = endpoint.Address, Binding = endpoint.Binding, ListenUri = endpoint.ListenUri, ListenUriMode = endpoint.ListenUriMode, Name = endpoint.Name + "_" + count.ToString() }; count++; foreach (IEndpointBehavior eb in endpoint.Behaviors) { copy.Behaviors.Add(eb); } if (endpoint.Address != null && endpoint.Address.Uri.Host == MbaServiceHostFactory.DummyHost) { EndpointAddressBuilder endpointAddressBuilder = new EndpointAddressBuilder(endpoint.Address); endpointAddressBuilder.Uri = MbaServiceHostFactory.GetNormalizedUri(ba, endpoint.Address.Uri.PathAndQuery); copy.Address = endpointAddressBuilder.ToEndpointAddress(); } if (endpoint.ListenUri != null && endpoint.ListenUri.Host == MbaServiceHostFactory.DummyHost) { copy.ListenUri = MbaServiceHostFactory.GetNormalizedUri(ba, endpoint.ListenUri.PathAndQuery); ; } // make sure we're not re-adding an endpoint that already exists because it had an absolute Uri in config if (!singleEndpoints.Exists(se => se.Address.Uri == copy.Address.Uri)) { service.Description.Endpoints.Add(copy); } } }
For brevity I’ve left out the helper method (GetNormalizedUri) that this uses to build the absolute addresses that you can find in the solution code. All you need to do now is use this as your factory in your .svc file and you’re good to go. In the provided code, I’ve called this factory MbaServiceHostFactory (Mba: Multiple Base Addresses) and I’ve also implemented a MbaServiceHostFactory<T> for cases in which you want the base implementation to be something other than ServiceHostFactory, so if you want a WebServiceHostFactory as your base behavior you can either change MbaServiceHostFactory to inherit from it, or use MbaServiceHostFactory<WebServiceHostFactory> instead.
The WCF team is building an out of the box solution for this issue in the .NET 4.0 release, but if for whatever reason you can’t upgrade, I strongly recommend using this approach.
You can download the sources here.
3 comments:
The code looks great and I thank you for all your hard work. Something seems off with my environment though. The return from CreateServiceHostCore() seems correct. In the debugger I can see there are 4 [service.Description.Endpoints] values but only a couple of the endpoints allow me to see the service when entering the URL into a web browser. The others simply give me a blank screen - no errors or anything to indicate success or failure. This is a step forward from the errors I was receiving, I suppose. ;)
Any ideas?
This is an example what I'm seeing:
BaseAddress
{http://svcserv1.domain.com:818/Services/ReportService/Reports.svc}
ListenURIs
{http://svcserv1.domain.com:818/Services/ReportService/Reports.svc}
{http://svcserv/Services/ReportService/Reports.svc}
{http://svcserv1.domain.com:818/Services/ReportService/Reports.svc/mex}
{http://svcserv/Services/ReportService/Reports.svc/mex}
Going to the FQDN:port URL works fine, but if I go to the alias I receive a blank page. I'm assuming this is due to the single BaseAddress? Is it possible to respond to multiple URL entries?
the adresses ending in mex are for the mex endpoint, that's just for that (Metadata EXchange). the intranet URI (http://svcserv/Services/ReportService/Reports.svc) should work just fine.
Post a Comment