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:
[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...
<#@ 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:
[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:
static void Main(string[] args)
{
IUnityContainer container = new UnityContainer();
container.RegisterType<IOperationsManager, SingleProcessDeploymentOperationsManager>(
new ContainerControlledLifetimeManager());
container.RegisterType<ICalculatorComponent, CalculatorComponentImpl>();
ServiceLocator.SetServiceLocator(new UnityServiceLocator(container));
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
:
[Operation("Calculator")]
public class AddOperation : OperationBase<double>
{
}
[Operation("Calculator")]
public class SubtractOperation : OperationBase<double>
{
}
Which would generate contract like this:
[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):
[Operation("Calculator", Generate = OperationGeneration.Both)]
public class AddOperation : OperationBase<double>
{
}
Generates contract:
[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:
[Operation("Calculator",
Generate = OperationGeneration.Async,
IsReportingProgress = true,
IsSupportingCancel = true,
ExpectedCompletionTimeMilliseconds = 60000,
SuggestedPollingIntervalMilliseconds = 2000
)]
public class AddOperation : OperationBase<double>
{
?}
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:
- Request parameters (provided by the party consuming the service)
- A
IOperationsManager
instance to be passed to OperationBase<T>
- Dependency injected parameters
To stick our AddOperation
example, here is (again) the operation code...
[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:
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
:
[Operation("Calculator")]
[OperationFaultContract(typeof(CalculatorFault))]
public class AddOperation : OperationBase<double>
{
}
Based on this operation, the auto-generated contract might look like this:
[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