Click here to Skip to main content
15,867,308 members
Articles / DevOps / Testing

Putting WCF to Work

,
Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
14 Jan 2016CPOL7 min read 13.8K   152   12  
Infrastructure for large scale WCF services layer development

Introduction

This article is a summary of the issues that concerned us and the solutions we used when we approached creating our web services layer using WCF. It all amounts neatly into a set of assemblies containing code and tools that can serve as the infrastructure for your service layer or as an inspiration for your new development.

One of the main motivations for publishing this article is to get feedback. Don't hold back.

A word to the wise: as of late, REST based services as included in ASP.NET Web API seem to be the preferred child in the communication stack at Microsoft. While WCF is definitely better suited for some scenarios, this should be factored in. Make an informed decision.

Background

As we started developing our WCF services layer, we were looking to address the following issues:

  • Avoid boilerplate code
  • Provide unified organization for big projects
  • Provide sync and async functionality
  • Avoid retaining state as much as possible
  • Provide error handling infrastructure
  • Support testability
  • Support Authentication and Authorization
  • Maintain transport and hosting agnostic services

We address each issue below.

Using the Code

You would probably not want to just 'use the code' but this is what you would do when using it:

Step 1: Prepare Your Solution

In your solution, you would have the following projects:

  • Operations - Your services code. Development work is done here (references Ziv.ServiceModel - the core runtime assembly).
  • Contracts - An assembly containing the services code. All code here is auto generated using the built-in Visual Studio code generation platform - T4 templates (references Ziv.ServiceModel, Ziv.ServiceModel.CodeGeneration and the Operations project.
  • Services - An assembly containing the services code. Code here is also auto-generated (references Contracts project and all its dependencies).
  • Host - The deployed hosting project - mostly about bootstrapping the whole thing, usually your IIS project (references the core, the operations and the generated code. Does not need the code generation stuff Ziv.ServiceModel.CodeGeneration).

For the complete example, download the solution attached to this article, or check it out on GitHub.

Step 2: Write Your Code

In your operations project, for each service method you want to expose, do the following:

  • Create a class derived from OperationBase<TResult> and decorate it with OperationAttribute.
  • Create a constructor that takes the parameters your operation expects (if any), the IOperationsManager instance you need pass on to the base class constructor, and the dependencies you need injected to perform your task (order matters here, see explanation later).
    Make sure you keep the parameters and dependencies you need in private class fields.
  • Implement the abstract Run() method.

Example operation:

C#
[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    private ICalculatorComponent _calculator;
    private AddOperationParameters _parameters;
    
    public AddOperation(
                AddOperationParameters parameters,
                IOperationsManager operationsManager,
                ICalculatorComponent calculator)
         : base(operationsManager)
    {
        _calculator = calculator;
        _parameters = parameters;
    }

    protected override double Run()
    {
        return _calculator.Add(_parameters.Addend1, _parameters.Addend2);
    }     
}

Step 3: Generate Services and Contracts

In both Contracts and Services projects, do the following:

  • Build the project while it is still empty (causing referenced assemblies to be copied to the project target folder).
  • Create a text template (*.tt) file.
  • In this file, reference the Operations project assembly and include the appropriate template from the CodeGeneration library (see examples below).
  • Right click on the *.tt file and run custom tool.

The T4 templates would run and generate your contract interfaces and your service classes in new *.cs files.

Example for Contracts.tt file...

XML
<#@ assembly name="$(SolutionDir)\Sample.Operations\bin\Debug\Sample.Operations.dll" #>
<#@ include file="$(SolutionDir)\Ziv.ServiceModel.CodeGeneration\Templates\Services.ttinclude" #>

... and the code it would generate:

C#
[ServiceContract]
public interface ICalculatorService
{
     #region Operation Add

     [OperationContract]
     double Add(AddOperationParameters parameters);

     #endregion
}

See more examples below and in the attached solution.

Step 4: Host Your Services

You can host the generated classes with WCF in any deployment scheme. All you need to do is to provide the Ziv.ServiceModel.Activation.DependencyInjectionServiceHostFactory as your service host factory and register your dependency injection framework by calling ServiceLocator.SetServiceLocator().

You should also register an IOperationsManager instance with the dependency injection container (has to be a singleton).

Here is a very simple example for console-based host project, using Unity as a DI framework:

C#
static void Main(string[] args)
{
    // Setup DI configuration:
    IUnityContainer container = new UnityContainer();    
    container.RegisterType<IOperationsManager, SingleProcessDeploymentOperationsManager>(
        new ContainerControlledLifetimeManager());
    container.RegisterType<ICalculatorComponent, CalculatorComponentImpl>();
    ServiceLocator.SetServiceLocator(new UnityServiceLocator(container));

    // Setup hosting:
    var factory = new DependencyInjectionServiceHostFactory();
    var baseAddress = new Uri("http://localhost:54321/services");
    var serviceHost = factory.CreateServiceHost(
                                typeof(CalculatorService).AssemblyQualifiedName, 
                                new Uri[] { baseAddress });
    serviceHost.Open();

    Console.ReadKey();
}

You can find more production suitable, web-based hosting example in the attached example solution. There you can also run the custom tool on the web.tt in the hosting project and you have a running hosting solution with all your new stuff ready to be served.

How It Works

The operation classes are developed in a WCF agnostic way, and all the plumbing is generated using the standard Visual Studio T4 templating. The IOperationsManager is the glue that provides the management services for this whole operation.

Avoid Boilerplate Code

When you write your business logic in operation class, you can be focused on it exclusively. All of the surrounding work is done for you:

  • The SOAP contracts and the corresponding WCF service classes are auto-generated by T4 templates based on the operation signature.
  • Asynchronous invocation, operation's state management and error handling - are all done in the OperationBase<T> and in the injected IOperationManager.

Provide Unified Organization for Big Projects

The arrangement of multiple operation in hierarchical structure is almost automatic. Your operations' namespace hierarchy would be reflected in the namespaces of the auto-generated code (contracts and service classes) and in the URL schema which would be used to access services. To consolidate multiple operations into one service, you can supply the same 'short name' to every operation in its OperationAttribute:

C#
[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

[Operation("Calculator")]
public class SubtractOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

Which would generate contract like this:

C#
[ServiceContract]
public interface ICalculatorService     
{         
    #region Operation Add

    [OperationContract]
    double Add(AddOperationParameters parameters);

    #endregion

    #region Operation Subtract

    [OperationContract]
    double Subtract(SubtractOperationParameters parameters);

    #endregion
}

Provide sync and async Functionality

Use OperationAttribute.Generate to request sync and/or async operations on your exposed contract (and service):

C#
[Operation("Calculator", Generate = OperationGeneration.Both)]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

Generates contract:

C#
[ServiceContract]
public interface ICalculatorService     
{         
    #region Operation Add

    [OperationContract]
    double Add(AddOperationParameters parameters);

    [OperationContract]
    OperationStartInformation AddAsync(AddOperationParameters parameters);

    [OperationContract]
    OperationStatus<double> AddGetStatus(Guid operationId);

    [OperationContract]
    void AddCancel(Guid operationId);

    #endregion
}

During a long running operation, call ReportProgress() from your operation code to report process progress; call IsCancelationPending() to check if the user has requested cancellation, and report completing cancellation by calling ReportCancelationCompleted(). These methods are defined on OperationBase<T> and report all developments to the IOperationsManager.

For long running operations, you can also use the OperationAttribute to provide the client with some data to help it decide when to check for a result and when to poll for progress report:

C#
[Operation("Calculator",
    Generate = OperationGeneration.Async,
    IsReportingProgress = true,
    IsSupportingCancel = true,    
    ExpectedCompletionTimeMilliseconds = 60000,    
    SuggestedPollingIntervalMilliseconds = 2000
)]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
?}

When client calls AddAsync method, it would get that information in the returned OperationStartInformation.

Avoid Retaining State As Much As Possible

Operations are expected to retain no state. All state, including return values, errors, progress state and cancellation states, are managed by the IOperationsManager passed to the OperationBase<T>. This way, long running operations just do what they are supposed to do and not deal with the clients polling for current state, which is very useful for request-response protocols such as HTTP.

Operations depending on other code components should use dependency injection to let the system know what it is they need, and the system would be sure to provide them using the dependency resolver set by ServiceLocator.SetServiceLocator().

For the DI mechanism to work well in conjunction with the other operation's inputs, the operation constructor should take parameters in the following order:

  1. Request parameters (provided by the party consuming the service)
  2. A IOperationsManager instance to be passed to OperationBase<T>
  3. Dependency injected parameters

To stick our AddOperation example, here is (again) the operation code...

C#
[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
    private ICalculatorComponent _calculator;
    private AddOperationParameters _parameters;
    
    public AddOperation(
                AddOperationParameters parameters,
                IOperationsManager operationsManager,
                ICalculatorComponent calculator)
         : base(operationsManager)
    {
        _calculator = calculator;
        _parameters = parameters;
    }

    protected override double Run()
    {
        return _calculator.Add(_parameters.Addend1, _parameters.Addend2);
    }     
}

... and the service class that auto-generated based on it:

C#
public sealed class CalculatorService : ServiceBase, ICalculatorService
{
    private readonly IOperationsManager _operationsManager;
    private readonly ICalculatorComponent _calculator;

    public CalculatorService(IOperationsManager operationsManager, ICalculatorComponent calculator)
        : base(operationsManager)
    {
         _operationsManager = operationsManager;
         _calculator = dataApplicationProvider;
    }

    #region Operation Add

    public double Add(AddOperationParameters parameters)
    {
        return (double)DoOperation(GetOperationAdd(parameters)).Result;
    }

    private AddOperation GetOperationAdd(AddOperationParameters parameters)
    {
        return new AddOperation(parameters, _operationsManager, _calculator);
    }

    #endregion
}

Note how the parameters of AddOperation's constructor are mapped to various elements of the generated service class.

Provide Error Handling Infrastructure

Error handling is based on standard WCF/SOAP error handling. To decorate contract interfaces with FaultContractAttibute, decorate the operation class with OperationFaultAttribute:

C#
[Operation("Calculator")]
[OperationFaultContract(typeof(CalculatorFault))]
public class AddOperation : OperationBase<double>
{
    // Class body omitted for briefly...
}

Based on this operation, the auto-generated contract might look like this:

C#
[ServiceContract]
public interface ICalculatorService
{
     #region Operation Add

     [OperationContract]
     [FaultContract(typeof(CalculatorFault))]
     double Add(AddOperationParameters parameters);

     #endregion
}

Support Testability

As operations are created stateless, it is very easy to provide a mock replacement for IOperationsManager and the other dependencies and have them run as part of an automated test. All state can be observed from the IOperationsManager mock.

Support Authentication and Authorization

Authentication and Authorization rely on .NET and WCF infrastructure. Namely decorating Operation.Run() with PrincipalPermissionAttribute.

Maintain Transport and Hosting Agnostic Services

Generated services are plain WCF with no reliance on any specific protocol. Implementation of an Authentication and Authorization modes availability varies between WCF bindings, so some tweaking of the pipeline might be required for some deployment scenarios.

Conclusion

WCF does a great job in abstraction of working with sophisticated network protocols. However, with regard to development management of big project and complex products, it still might be considered a low-level framework. Our solution address the next level of abstraction, resulting in more efficient development of the service layer.

Please note that the current stage of our library is focused on the server side of distributed system. Client side received a little attention to streamline some WCF odd behaviors like the way that fault-state objects are handled and disposed, but it is still work in progress.

We hope this library has achieved its general goal of simplifying service development while keeping it powerful. If you have any thoughts, criticism or comments, let us know.

History

  • Version 1.0 January 2016

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Ziv systems, Israel
Israel Israel
Starting with Apple IIe BASICA, and working my way through Pascal, Power Builder, Visual basic (and the light office VBA) C, C++, I am now a full stack developer and development manager. Mostly with MS technologies on the server side and javascript(typescript) frameworks on the client side.

Written By
Software Developer Ziv Systems
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --