Getting WCF Services in a Silverlight solution to play nice on deployment

Posted by brendonpage on Geeks with Blogs See other posts from Geeks with Blogs or by brendonpage
Published on Thu, 03 Jun 2010 17:07:40 GMT Indexed on 2010/06/03 18:14 UTC
Read the original article Hit count: 710

Filed under:

I have come across 2 issues with deploying WCF services in a Silverlight solution, admittedly the one is more of a hiccup, and only occurs if you take the easy way out and reference your services through visual studio.

The First Issue

This occurs when you deploy your WFC services to an IIS server. When browse to the services using your web browser, you are greeted with “This collection already contains an address with scheme http.  There can be at most one address per scheme in this collection.”. When you make a call to this service from your Silverlight application, you get the extremely helpful “NotFound” error, this error message can be found in the error property of the event arguments on the complete event handler for that call.

As it did with me this will leave most people scratching their head, because the very same services work just fine on the ASP.NET Development Web Server and on my local IIS server. Now I’m no server/hosting/IIS expert so I did a bit of searching when I first encountered this issue. I found out this happens because IIS supports multiple address bindings per protocol (http/https/ftp … etc) per web site, but WCF only supports binding to one address per protocol. This causes a problem when the WCF service is hosted on a site with multiple address bindings, because IIS provides all of the bindings to the host factory when running the service. While this problem occurs mainly on shared hosting solutions, it is not limited to shared hosting, it just seems like all shared hosting providers setup sites on their servers with multiple address bindings.

For interests sake I added functionality to the example project attached to this post to dump the addresses given to the WCF service by IIS into a log file. This was the output on the shared hosting solution I use:

http://mydomain.co.za/Services/TestService.svc
http://www.mydomain.co.za/Services/TestService.svc
http://mydomain-co-za.win13.wadns.net/Services/TestService.svc
http://win13/Services/TestService.svc

As you can see all these addresses are for the http protocol, which is where it all goes wrong for WCF.

Fixes for the First Issue

There are a few ways to get around this. The first being the easiest, target .NET 4! Yes that's right in .NET 4 WCF services support multiple addresses per protocol. This functionality is enabled by an option, which is on by default if you create a new project, you will need to turn on if you are upgrading to .NET 4. To do this set the multipleSiteBindingsEnabled property of the serviceHostingEnviroment tag in the web.config file to true, as shown below:

<system.serviceModel>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Beware this ONLY works in .NET 4, so if you don’t have a server with .NET 4 installed on that you can deploy to, you will need to employ one of the other work a rounds.

The second option will work for .NET 3.5 & 4. For this option all you need to do is modify the web.config file and add baseAddressPrefixFilters to the serviceHostingEnviroment tag as shown below:

<system.serviceModel>
    <serviceHostingEnvironment>
        <baseAddressPrefixFilters> 
            <add prefix="http://www.mydomain.co.za"/>
        </baseAddressPrefixFilters>
    </serviceHostingEnvironment>
</system.serviceModel>

These will be used to filter the list of base addresses that IIS provides to the host factory. When specifying these prefix filters be sure to specify filters which will only allow 1 result through, otherwise the entire exercise will be pointless. There is however a problem with this work a round, you are only allowed to specify 1 prefix filter per protocol. Which means you can’t add filters for all your environments, this will therefore add to the list of things to do before deploying or switching dev machines.

The third option is the one I currently employ, it will work for .NET 3, 3.5 & 4, although it is not needed for .NET 4. For this option you create a custom host factory which inherits from the ServiceHostFactory class. In the implementation of the ServiceHostFactory you employ logic to figure out which of the base addresses, that are give by IIS, to use when creating the service host. The logic you use to do this is completely up to you, I have seen quite a few solutions that simply statically reference an index from the list of base addresses, this works for most situations but falls short in others. For instance, if the order of the base addresses where to change, it might end up returning an address that only resolves on the servers local network, like the last one in the example I gave at the beginning. Another instance, if a request comes in on a different protocol, like https, you will be creating the service host using an address which is on the incorrect protocol, like http.

To reliably find the correct address to use, I use the address that the service was requested on. To accomplish this I use the HttpContext, which requires the service to operate with AspNetCompatibilityRequirements set on. If for some reason running you services with AspNetCompatibilityRequirements on isn’t an option, you can still use this method, you will just have to come up with your own logic for selecting the correct address.

First you will need to enable AspNetCompatibilityRequirements for your hosting environment, to do this you will need to set it to true in the web.config file as shown below:

<system.serviceModel>
    <serviceHostingEnvironment AspNetCompatibilityRequirements="true" />
</system.serviceModel>

You will then need to mark any services that are going to use the custom host factory, to allow AspNetCompatibilityRequirements, as shown below:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class TestService
{
}

Now for the custom host factory, this is where the logic lives that selects the correct address to create service host with. The one i use is shown below:

public class CustomHostFactory : ServiceHostFactory
{
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        //
        // Compose a prefix filter based on the requested uri
        //
        string prefixFilter = HttpContext.Current.Request.Url.Scheme + "://" + HttpContext.Current.Request.Url.DnsSafeHost;
        if (!HttpContext.Current.Request.Url.IsDefaultPort)
        {
            prefixFilter += ":" + HttpContext.Current.Request.Url.Port.ToString() + "/";
        }

        //
        // Find a base address that matches the prefix filter
        //
        foreach (Uri baseAddress in baseAddresses)
        {
            if (baseAddress.OriginalString.StartsWith(prefixFilter))
            {
                return new ServiceHost(serviceType, baseAddress);
            }
        }

        //
        // Throw exception if no matching base address was found
        //
        throw new Exception("Custom Host Factory: No base address matching '" + prefixFilter + "' was found.");
    }
}

The most important line in the custom host factory is the one that returns a new service host. This has to return a service host that specifies only one base address per protocol. Since I filter by the address the request came on in, I only need to create the service host with one address, since this address will always be of the correct protocol.

Now you have a custom host factory you have to tell your services to use it. To do this you view the markup of the service by right clicking on it in the solution explorer and choosing “View Markup”.

viewservicemarkup

Then you add/set the value of the Factory property to the full namespace path of you custom host factory, as shown below.

markupfactoryproperty

And that is it done, the service will now use the specified custom host factory.

The Second Issue

As I mentioned earlier this issue is more of a hiccup, but I thought worthy of a mention so I included it. This issue only occurs when you add a service reference to a Silverlight project. Visual Studio will generate a lot of code for you, part of that generated code is the ServiceReferences.ClientConfig file. This file stores the endpoint configuration that is used when accessing your services using the generated proxy classes. Here is what that file looks like:

<configuration>
    <system.serviceModel>
        <bindings>
            <customBinding>
                <binding name="CustomBinding_TestService">
                    <binaryMessageEncoding />
                    <httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
                </binding>
                <binding name="CustomBinding_BrokenService">
                    <binaryMessageEncoding />
                    <httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
                </binding>
            </customBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:49347/services/TestService.svc"
                binding="customBinding" bindingConfiguration="CustomBinding_TestService"
                contract="TestService.TestService" name="CustomBinding_TestService" />
            <endpoint address="http://localhost:49347/Services/BrokenService.svc"
                binding="customBinding" bindingConfiguration="CustomBinding_BrokenService"
                contract="BrokenService.BrokenService" name="CustomBinding_BrokenService" />
        </client>
    </system.serviceModel>
</configuration>

As you will notice the addresses for the end points are set to the addresses of the services you added the service references from, so unless you are adding the service references from your live services, you will have to change these addresses before you deploy. This is little more than an annoyance really, but it adds to the list of things to do before you can deploy, and if left unchecked that list can get out of control.

Fix for the Second Issue

The way you would usually access a service added this way is to create an instance of the proxy class like so:

BrokenServiceClient proxy = new BrokenServiceClient();

Closer inspection of these generated proxy classes reveals that there are a few overloaded constructors, one of which allows you to specify the end point address to use when creating the proxy. From here all you have to do is come up with some logic that will provide you with the relative path to your services. Since my WCF services are usually hosted in the same project as my Silverlight app I use the class shown below:

public class ServiceProxyHelper
{
    /// <summary>
    /// Create a broken service proxy
    /// </summary>
    /// <returns>A broken service proxy</returns>
    public static BrokenServiceClient CreateBrokenServiceProxy()
    {
        Uri address = new Uri(Application.Current.Host.Source, "../Services/BrokenService.svc");
        return new BrokenServiceClient("CustomBinding_BrokenService", address.AbsoluteUri);
    }
}

Then I will create an instance of the proxy class using my service helper class like so:

BrokenServiceClient proxy = ServiceProxyHelper.CreateBrokenServiceProxy();

The way this works is “Application.Current.Host.Source” will return the URL to the ClientBin folder the Silverlight app is hosted in, the “../Services/BrokenService.svc” is then used as the relative path to the service from the ClientBin folder, combined by the Uri object this gives me the URL to my service. The “CustomBinding_BrokenService” is a reference to the end point configuration in the ServiceReferences.ClientConfig file. Yes this means you still need the ServiceReferences.ClientConfig file. All this is doing is using a different end point address than the one specified in the ServiceReferences.ClientConfig file, all the other settings form the ServiceReferences.ClientConfig file are still used when creating the proxy.

I have uploaded an example project which covers the custom host factory solution from the first issue and everything from the second issue. I included the code to write a list of base addresses to a log file in my implementation of the custom host factory, this is not need for the custom host factory to function and can safely be removed.

Download (WCFServicesDeploymentExample.zip)

© Geeks with Blogs or respective owner