.NET

Ninject WCF Extensions for RESTful Services

A while ago I blogged about using Ninject for dependency injection with WCF Services. The advantage of using DI is that it allows you to achieve loose coupling in your application architecture, so that you’re not tightly bound to a particular infrastructure implementation, such as data access or logging. The problem with WCF services is that by default they are required to have a parameterless constructor, which does not play nice with DI containers, such as Ninject, which support injection of dependencies via constructor parameters.

I had the need recently to set up a REST-style WCF Services project and wanted to use Ninject for DI with it. Luckily, the Ninject WCF Extension project had been updated to support REST, so I updated the Nuget package and discovered the project would not compile. I found out that the static KernelContainer class had been deprecated, so I had to refactor my code to remove references to it. I also noticed there was no longer any need to derive the Global.asax code file from NinjectWcfApplication, because the extension now uses WebActivator to configure Ninject on application startup. A NinjectWebCommon.cs file is placed in an App_Start folder. There you simply add code to a RegisterServices method in order to load your Ninject modules and perform the bindings. (When updating the NuGet package on the other projects, I had to manually remove the App_Start folder.)

Typically when exposing a REST-type endpoint from a WCF service, you would leverage the ASP.NET UrlRoutingModule by adding a ServiceRoute to the RouteTable in the Application_Start method of your Global.asax.cs file. Things get a little tricky, however, if you want to expose both SOAP and REST endpoints from the same WCF service. In this case, you’ll want to supply a ServiceHostFactory-derived class when registering the service route, which let’s you specify an endpoint address that is different than the base HTTP address used for the SOAP endpoint.

public class RestServiceHostFactory<TServiceContract> : NinjectWebServiceHostFactory
{
    protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
    {
        ServiceHost host = base.CreateServiceHost(serviceType, baseAddresses);
        var webBehavior = new WebHttpBehavior
        {
            AutomaticFormatSelectionEnabled = true,
            HelpEnabled = true,
            FaultExceptionEnabled = true
        };
        var endpoint = host.AddServiceEndpoint(typeof(TServiceContract), new WebHttpBinding(), "Rest");
        endpoint.Name = "rest";
        endpoint.Behaviors.Add(webBehavior);
        return host;
    }
}

Here is code from Global.asax.cs which references RestServiceHostFactory:

public class Global : HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        RegisterRoutes();
    }
    private void RegisterRoutes()
    {
        // Add rest service route
        RouteTable.Routes.Add(new ServiceRoute("GreetingService",
            new RestServiceHostFactory<IGreetingService>(), typeof(GreetingService)));
    }
}

Browsing to the REST-endpoint, and appending /help to the url, displays a list of available operations.

In my original blog post on this topic I introduced a NinjectServiceHelper class that can be used by test classes to spin up matching services and clients on the fly. I refactored this class to support REST scenarios by adding the required endpoint behavior for the WebHttpBinding.

public class NinjectServiceHelper<TServiceContract, TServiceType> : IDisposable
{
    bool _disposed;
    public NinjectServiceHelper(IServiceBehavior serviceBehavior, string address, Binding binding)
    {
        // Create Ninject service host
        _serviceHost = new NinjectServiceHost(serviceBehavior, typeof(TServiceType));
        // Add endpoint
        _serviceHost.AddServiceEndpoint(typeof(TServiceContract), binding, address);
        // Add web behavior
        if (binding.GetType() == typeof(WebHttpBinding))
        {
            var webBehavior = new WebHttpBehavior
            {
                AutomaticFormatSelectionEnabled = true,
                HelpEnabled = true,
                FaultExceptionEnabled = true
            };
            _serviceHost.Description.Endpoints[0].Behaviors.Add(webBehavior);
        }
        // Add service metadata
        var metadataBehavior = new ServiceMetadataBehavior();
        if (binding.GetType() == typeof(BasicHttpBinding))
        {
            metadataBehavior.HttpGetEnabled = true;
            metadataBehavior.HttpGetUrl = new Uri(address);
        }
        _serviceHost.Description.Behaviors.Add(metadataBehavior);
        // Open service host
        _serviceHost.Open();
        // Init client
        var factory = new ChannelFactory<TServiceContract>(binding);
        _client = factory.CreateChannel(new EndpointAddress(address));
    }
    private readonly ServiceHost _serviceHost;
    public ServiceHost ServiceHost
    {
        get
        {
            if (_disposed)
                throw new ObjectDisposedException("NinjectServiceHelper");
            return _serviceHost;
        }
    }
    private readonly TServiceContract _client;
    public TServiceContract Client
    {
        get
        {
            if (_disposed)
                throw new ObjectDisposedException("NinjectServiceHelper");
            return _client;
        }
    }
    public void Dispose()
    {
        if (!_disposed)
        {
            ((IDisposable)_serviceHost).Dispose();
            ((IDisposable)_client).Dispose();
            _disposed = true;
        }
    }
}

The test class then loads a Ninject module in the TestFixtureSetup method that adds named bindings to the kernel for SOAP and REST WCF endpoints.

public class ServicesModule : NinjectModule
{
    public override void Load()
    {
        // Basic service host
        Kernel.Bind<NinjectServiceHelper<IGreetingService, GreetingService>>()
            .ToSelf()
            .Named("Basic")
            .WithConstructorArgument("address", "http://localhost:1234/GreetingService/Soap/")
            .WithConstructorArgument("binding", new BasicHttpBinding());
        // Tcp service host
        Kernel.Bind<NinjectServiceHelper<IGreetingService, GreetingService>>()
            .ToSelf()
            .Named("Tcp")
            .WithConstructorArgument("address", "net.tcp://localhost:9999/GreetingService/Tcp/")
            .WithConstructorArgument("binding", new NetTcpBinding());
        // Rest service host
        Kernel.Bind<NinjectServiceHelper<IGreetingService, GreetingService>>()
            .ToSelf()
            .Named("Rest")
            .WithConstructorArgument("address", "http://localhost:1234/GreetingService/Rest/")
            .WithConstructorArgument("binding", new WebHttpBinding());
    }
}

The test methods then obtain the appropriate helper instance by name.

private void Greeting_Soap(string protocol)
{
    // Arrange
    using (var helper = _kernel.Get<NinjectServiceHelper<IGreetingService, GreetingService>>(protocol))
    {
        using ((IDisposable)helper.Client)
        {
            // Act
            string greeting = helper.Client.Hello();
            // Assert
            Assert.That(greeting, Is.StringMatching("Hello"));
        }
    }
}
private void Greeting_Rest(string format)
{
    using (var helper = _kernel.Get<NinjectServiceHelper<IGreetingService, GreetingService>>("Rest"))
    {
        // Arrange
        var client = new WebClient();
        if (format == "Json")
            client.Headers.Add(HttpRequestHeader.Accept, "application/" + format);
        client.BaseAddress = helper.ServiceHost.Description.Endpoints[0].Address.ToString();
        // Act
        string result = client.DownloadString("Hello");
        string greeting;
        if (format == "Xml")
            greeting = SerializationHelper.DeserializeXml<string>(result);
        else if (format == "Json")
            greeting = SerializationHelper.DeserializeJson<string>(result);
        else
            throw new Exception("Format not supported: " + format);
        // Assert
        Assert.That(greeting, Is.StringMatching("Hello"));
    }
}

My SerializationHelper class simplifies the task of converting Xml and Json to and from CLR objects.

public class SerializationHelper
{
    public static string SerializeXml<T>(T obj)
    {
        using (var ms = new MemoryStream())
        {
            var serializer = new DataContractSerializer(typeof(T));
            serializer.WriteObject(ms, obj);
            string retVal = Encoding.Default.GetString(ms.ToArray());
            return retVal;
        }
    }
    public static T DeserializeXml<T>(string xml)
    {
        using (var reader = new StringReader(xml))
        {
            using (var xmlReader = XmlReader.Create(reader))
            {
                var serializer = new DataContractSerializer(typeof(T));
                var obj = (T)serializer.ReadObject(xmlReader);
                return obj;
            }
        }
    }
    public static string SerializeJson<T>(T obj)
    {
        using (var ms = new MemoryStream())
        {
            var serializer = new DataContractJsonSerializer(typeof(T));
            serializer.WriteObject(ms, obj);
            string retVal = Encoding.Default.GetString(ms.ToArray());
            return retVal; 
        }
    }
    public static T DeserializeJson<T>(string json)
    {
        using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
        {
            var serializer = new DataContractJsonSerializer(typeof(T));
            var obj = (T)serializer.ReadObject(ms);
            return obj; 
        }
    }
}

Ninject’s WCF extension makes it easy to build both SOAP and REST style WCF services that use dependency injection for apps that are loosely coupled to specific infrastructure implementations. The project examples up on GitHub also demonstrate self-hosting scenarios.

Download the code for this blog post here.

Reference: Ninject WCF Extensions for RESTful Services from our NCG partner Tony Sneed at the Tony Sneed’s Blog blog.

Related Articles

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button