2012-02-17

Applying WebInvoke without an Attribute

Haven't posted in a long time but thought I'd post this bit of WCF extensibility I was working on at home. There is nothing on the Internet about how to do this & I felt I should figure something out.

Scenario: You have legacy WCF services that you want to switch to a WebHttpBinding w/ JSON but don't want to update 1,337 service contracts to include a WebInvoke attribute, UriTemplates and Request/Response Body Types

In this scenario I'll be assuming use of IIS and Windows Process Activation Services to host your service.

The WebInvokeAttribute implements IOperationBehavior. So what we need to do is somewhere in the WCF stack apply this operation behavior programmatically to our Endpoint's Contract.Operations. Let's start from the bottom up & take a look at an extension method that does this.


/// <summary>
/// Automatically applies the Web Invoke behavior to all operations in the collection
/// </summary>
/// <param name="operations"></param>
public static void ApplyWebInvoke(this System.ServiceModel.Description.OperationDescriptionCollection operations)
{
foreach (var operation in operations)
{
var webInvoke = new System.ServiceModel.Web.WebInvokeAttribute
{
BodyStyle = System.ServiceModel.Web.WebMessageBodyStyle.Wrapped,
Method = "POST",
RequestFormat = System.ServiceModel.Web.WebMessageFormat.Json,
ResponseFormat = System.ServiceModel.Web.WebMessageFormat.Json,
UriTemplate = operation.Name
};

operation.Behaviors.Add(webInvoke);
}
}

In the above extension method I'm forcing everything to be a POST, everything to use JSON and keeping the UriTemplates simply as the Operation name. Now where do we call this method? Personally, I'm a fan of having custom ServiceHosts + ServiceHostFactories rather than BehaviorExtensions. Chances are it's OK for me to touch the code & hosting portion of the application that I don't need to rely solely on configuration to extend WCF. If I have to extend a service that I don't have access to the ServiceHost -- behavior extensions are the only way. But given the choice -- custom ServiceHostFactory every time. The biggest reason is that I find other developers can follow the stack easier. It's less confusing. "Oh this SVC file has a custom factory? Oh this custom factory applies this behavior! "

Now take a look at the Factory and the custom Service Host:

public class WebInvokeServiceHostFactory : System.ServiceModel.Activation.ServiceHostFactory
{
protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
return new WebInvokeServiceHost(serviceType, baseAddresses).ApplyWebInvoke();
}
}

public class WebInvokeServiceHost : System.ServiceModel.Web.WebServiceHost
{
public WebInvokeServiceHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses) { }

internal WebInvokeServiceHost ApplyWebInvoke()
{
foreach(var endpoint in Description.Endpoints)
{
endpoint.Contract.Operations.ApplyWebInvoke();
}

return this;
}
}


I'm applying the WebInvoke behavior after the ServiceHost has been instantiated. Now just update the .svc file to use the new Factory:

<%@ ServiceHost Service="MySite.Services.TestService" Factory="MySite.ServiceModel.WebInvokeServiceHostFactory" %>


As for the client...
Given our scenario of working with a legacy application, I'll start by assuming we aren't auto-generating Service References. So we just need to apply the same IOperationBehavior to the Client Endpoint's Contract.Operations just like the Service. Below is a sample client base.

public class SampleClientBase : ClientBase<ITestService>, ITestService
{
public SampleClientBase(string config) : base(config) { }

public SampleClientBase() : this("TestService")
{
ChannelFactory.Endpoint.Behaviors.Add(new WebHttpBehavior());

ChannelFactory.Endpoint.Contract.Operations.ApplyWebInvoke();
}

public SomeObject BasicTest(SomeObject someObject)
{
return Channel.BasicTest(someObject);
}
}


And that's all there is to it! Hopefully now you see why that loop is an extension method and just how easy it really is to switch over to WebHttp from NetTcp without having to update your contracts.

No comments: