Introduction
Design of WCF ServiceContracts must conform to the constraints of WSDL which precludes the concepts of ad hoc polymorphism, i.e. operator overloading. With the use of OperationContractAttribute one can disguise this problem on the service
side by mapping the overloaded OperationContracts implementations (implicitly implemented) to a unique name that then appears in the <operation/> element of the WSDL. While
this gives a (perhaps) acceptable story for the service side overloading implementation,
additional work is required to bring the overloading to the client side. A specialized proxy can be developed for each service contract that again leverages OperationContractAttribute to handle the name mapping.1 However, this technique increases the development complexity and increases the deployment footprint.
If one has the luxury of implementing WCF services for an environment in
which the DataContractSerializer can be utilized, another alternative arises by
leveraging KnownTypes. One can design DataContract hierarchies with abstract
base classes which are used to parameterize the OperationContracts. The
serializer can then move subclasses of the DataContract parameter classes over
the wire transparently, and WCF will materialize the specialization when the operation is dispatched. This effectively provides a form of overloading by allowing single
OperationContract to have a wide variety of actual parameterizations.
However useful this is in ServiceContract management, the OperationContract implementation becomes rapidly unmanageable without further infrastructure support. Each OperationContract individually must handle every possible parameter specialization in some way. This article describes an implementation of the Bridge Pattern that solves this problem cleanly and leads to a complete separation of the implementation of the OperationContract from the code that produces its results. The side effect is to provide a very clean service implementation
pattern subject to enhancement by normal subclassing techniques. ServiceContract implementation classes become largely boilerplate, and service
implementations can potentially be injected by an IOC at runtime.
Background
The Bridge Pattern can be viewed as a form of multiple dispatch. The four principal entities in the pattern are
- The Abstraction which defines the abstract, externally exposed interface and also maintains a reference to the actual implementation, typically in a member variable of type Implementor
- The RefinedAbstraction which realizes the Abstraction
- The Implementor which defines the interface for the implementation
- The ConcreteImplementor which realizes the Implementor
Operationally, when the RefinedAbstraction is constructed it must initialize the Abstraction’s implementor variable with a specific instance of the ConcreteImplementor. Subsequent calls on the RefinedAbstraction interface implementation are delegated by the Abstraction into an appropriate implementation in the ConcreteImplementor.
By using reflection in .NET the nature of the Abstraction can be greatly generalized within the context of the WCF service implementation. The need for an explicit definition of Implementor interface can be eliminated by adopting naming and parameter signature conventions for the ConcreteImplementor. The convention used herein is that a valid delegation method in a ConcreteImplementor must match the name and signature of an OperationContract implemented by the service. The Abstraction delegation process then needs minimal understanding of the WCF interface.
When implementing WCF ServiceContracts, one may use either implicit or explicit implementation of the interface. Implicit implementations take the form of public methods of the correct signature in the service class. Explicit implementations take the form of private methods of the correct signature in the service class with operation names qualified with the ServiceContract name. While the delegator described below handles either, the full realization of Separation of Concerns between the RefinedAbstraction and the ConcreteImplementor can only be achieved with explicit implementations. Also explicit implementations allow OperationContracts with the same name and parameter structure to have separate implementations if necessary.
The Contracts
The WCF services implemented in this article take the form of a stateful add/subtract calculator whose parameters can be either integers or string representations of integers. These parameters types are described by the MathArgument
DataContract hierarchy.
namespace Samples.CalculatorService.Contract
{
[DataContract(IsReference = true)]
[KnownType(typeof(IntArgument))]
[KnownType(typeof(StringArgument))]
public abstract class MathArgument
{
}
[DataContract(IsReference = true)]
public class IntArgument : MathArgument
{
[DataMember]
public int Value;
}
[DataContract(IsReference = true)]
public class StringArgument : MathArgument
{
[DataMember]
public string Value;
}
}
The service supports two contracts with different interface names but identical OperationContracts.
namespace Samples.CalculatorService.Contract
{
[ServiceContract(Namespace = "http://Samples/CalculatorService/",
SessionMode = SessionMode.Required)]
public interface ICalculatorServiceB
{
[OperationContract(IsOneWay = false)]
int Add(MathArgument number);
[OperationContract(IsOneWay = false)]
int Subtract(MathArgument number);
}
:
[ServiceContract(Namespace = "http://Samples/CalculatorService/",
SessionMode = SessionMode.Required)]
public interface ICalculatorServiceA
{
[OperationContract(IsOneWay = false)]
int Add(MathArgument number);
[OperationContract(IsOneWay = false)]
int Subtract(MathArgument number);
}
}
The Abstraction
The Abstraction is realized as the ServiceBase
class. ServiceBase
's constructor argument initializes the ConcreteImplementor instance _implementor
. The generic Type parameter of the ServiceBase
is a subclass of ServiceBase
. If the subclass does not have a ServiceBehaviorAttribute, all subsequent dispatches attempts fault because the caller cannot be determined to be a valid WCF operation dispatch point. If the subclass has the ServiceBehaviorAttribute, then constructor fills _callSites
with a list of all valid dispatch point names by analyzing each ServiceContract on the service class.
ServiceBase's
single protected method MethodDispatcher
performs the delegation between the RefinedAbstraction and the ConcreteImplementor. Because of the overhead of validating the delegated calls reflectively, the MethodDispatcher
caches each successfully resolved delegation for future use.
namespace Samples.CalculatorService.Infrastructure.Service
{
public class ServiceBase<t> where T : class
{
private readonly HashSet<string> _callSites = new HashSet<string>();
private readonly object _implementor;
private readonly Dictionary<dispatchkey,> _methodCache =
new Dictionary<dispatchkey,>(new DispatchKeyComparer());
public ServiceBase(object implementor)
{
_implementor = implementor;
Array.ForEach(FindCallSites(), s => _callSites.Add(s));
}
:
protected virtual object MethodDispatcher(MethodBase methodBase, params object[] parameters)
{
DispatchEntry delegatedCall;
var key = new DispatchKey(methodBase.Name, parameters);
if (!_methodCache.TryGetValue(key, out delegatedCall))
{
if (!_callSites.Contains(methodBase.Name))
{
throw new ServiceMethodDispatchException(
ServiceMethodDispatchException.IllegalCallerMessageFormatter(methodBase));
}
delegatedCall = LoadDelegationDetails(methodBase, key);
_methodCache.Add(key, delegatedCall);
}
return delegatedCall.DelegationTarget.Invoke(delegatedCall.DelegationInstance, parameters);
}
:
}
}
</dispatchkey,></dispatchkey,></string></string>
The key of the cache is composite, formed of the OperationContract name and the ordered array of associated parameter types.
LoadDelgationDetails
performs reflective analysis of the caller (RefinedAbstraction) and the ConcreteImplementor to determine the correct delegation.
The RefinedAbstractions
The RefinedAbstractions are the two WCF service classes,
CalculatorService
and
CalculatorExtender
.
CalculatorService
fully implements the service contracts (one method implicitly), delegates the implementation to the
MethodDispatcher, and
casts the return value appropriately.
namespace Samples.CalculatorService
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public partial class CalculatorService : ServiceBase, ICalculatorService, ICalculatorServiceA
{
public CalculatorService(object implementor) : base(implementor)
{
}
public CalculatorService() : base(new CalculatorImplementation())
{
}
public int Subtract(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] {number});
}
int ICalculatorService.Add(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] {number});
}
int ICalculatorServiceA.Add(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] { number });
}
int ICalculatorServiceA.Subtract(MathArgument number)
{
return (int)MethodDispatcher(MethodBase.GetCurrentMethod(),
new object[] { number });
}
}
}
CalculatorExtender
however is much simpler in that it simply inherits from
CalculatorService
and provides a different implementation of the ConcreteImplementor. The WCF interface implementation does not have to be re-implemented.
namespace Samples.CalculatorService
{
public class CalculatorExtender : CalculatorService
{
public CalculatorExtender() : base( new ExtenderImplementation())
{
}
}
}
The ConcreteImplementors
The ConcreteImplementors,
CalculatorImplementation
and
ExtenderImplementation
, provide implementations that calculate the values for the services.
CalculatorImplementation
has one function for every possible parameterization of an OperationContract manifested by the WCF interfaces in concrete types. In this case there is one function for each concrete
MathArgument
that can be deserialized by WCF.
Two of the functions are marked by the
ExplicitMethodDispatchAttribute
. The occurrence of this attribute signals the dispatcher to delegate to the decorated method when processing calls for the specified OperationContracts (in
ICalculatorServiceA
in this case). While this couples the implementor to the ServiceContract (not the service class), it provides a means for different implementations for the two interfaces to be provided. If the
ExplicitMethodDispatchAttributes
are removed, the corresponding OperationContracts are delegated to the same implementation function (the one whose name is not suffixed with "A").
namespace Samples.CalculatorService
{
public partial class CalculatorImplementation
{
protected int total { get; set; }
protected virtual int Add(IntArgument number)
{
this.total += number.Value;
return this.total;
}
protected virtual int Add(StringArgument number)
{
this.total += Convert.ToInt32(number.Value);
return this.total;
}
protected virtual int Subtract(IntArgument number)
{
this.total -= number.Value;
return this.total;
}
[ExplicitMethodDispatch(typeof(ICalculatorServiceA), "Add")]
protected virtual int AddA(StringArgument number)
{
this.total += 3 * Convert.ToInt32(number.Value);
return total;
}
[ExplicitMethodDispatch(typeof(ICalculatorServiceA), "Subtract")]
protected virtual int SubtractA(StringArgument number)
{
this.total -= 3 * Convert.ToInt32(number.Value);
return this.total;
}
protected virtual int Subtract(StringArgument number)
{
return total -= Convert.ToInt32(number.Value);
}
}
}
Similarly to the service class, the
ExtenderImplementation
simply inherits from the
CalculatorImplementation
and overrides some of its functions. Note that one of the overrides is to a method decorated with the
ExplicitMethodDispatchAttribute
; the attribute does not have to be repeated for correct delegation to occur.
namespace Samples.CalculatorService
{
public class ExtenderImplementation : CalculatorImplementation
{
protected override int Add(IntArgument number)
{
total += number.Value * 2;
return total;
}
protected override int SubtractA(StringArgument number)
{
this.total -= Convert.ToInt32(number.Value);
return this.total;
}
}
}
One should also note that the ConcreteImplementors do 'funny math'. Input values in some cases are scaled to provide easily recognizable results in the unit tests that verify the dispatching path.
Exceptions
If dynamic resolution of the delegated call fails, the
MethodDispatcher
throws a
ServiceMethodDispatchException
at runtime containing one of two messages. Both indicate a problem in the service development, not execution.
- The UndispatchableCall message indicates that the dispatcher could not find a ConcreteImplementor method conforming to the parameterization of the incoming WCF call. This occurs, for example, if a parameter's DataContract hierarchy gains a new subclass but no corresponding implementation support is provided.
- The IllegalCall message indicates that a call was attempted from a location other than that of a defined OperationContract implementation. WCF interfaces using the
MethodDispatcher
must be marked with the ServiceContractAttribute
for validation to pass.
Using the code
The provided sample solution contains three projects (VS2012). The CalculatorService contains all of the classes described above, plus provides a WCF console host for the CalulatorExtender service over nettcp. The CalculatorClient provides a nettcp client for that service. To run as WCF services, start the CalulatorService (e.g. Debug/Start New Instance). After the host is console window opens, the Calculator Client may be similarly started.
ServiceImplTests contains xUnit tests against the service class infrastructure. This is all POCO and does not leverage WCF transport.
Points of Interest
- When using explicit interface implementations all WCF service classes are essentially boiler plate. This means creation of the code artifact could be easily delegated to a code generation process, reducing development effort. These could even be packaged in an independent assembly.
- Customization by subclassing in both the RefinedAbstraction and the ConcreteImplementor makes it easier to evolve the code with less repetitive code in the customizations.
- The binding of the ConcreteImplementor to the Abstraction can be performed by Dependency Injection making the delegation runtime swappable without re-implementing the RefinedAbstractions.
Alternatives
IDispatchOperationSelector
WCF's operation call dispatching pipeline offers an optional interception point at which the call can be redirected to an alternative implementation, very similarly to the process included herein. The functionality required must be encapsulated in an EndPointBehavior and attached to the EndpointDispatcher for each service EndPoint. Lowy shows an example of this in his book.2 Encapsulating the dispatching in an EndPointBehavior would completely eliminate the ServiceBase
class as shown here. The Abstraction implementation would then reside in the EndPointBehavior, and one call dispatch would be eliminated from the pipeline. The process of injecting the ConcreteImplementor would then be significantly different as well.
Assignable Parameters
The Bridge Pattern itself allows for the possibility of parameter coercion prior to the delegated call. While this implementation is based on exact parameter Type matching, this constraint could be relaxed to allow match to occur if the incoming parameter can be assigned to a ConcreteImplementor's parameter, i.e. the OperationContract could have a parameter of Type of IEnumerable<T> and match a ConcreteImplementor parameter of List<T>.
To enable a similar loosening in an IDispatchOperationSelector implementation, yet another EndpointBehavior implementing IParameterInspector would be likely be required.
References
1Lowy, Juval, Programming WCF Services, Third Edition: Mastering WCF and the Azure
AppFabric Service Bus. Sabastopol, CA, USA: O'Reilley Media, Inc., August, 2010, pp. 83-85.
2Ibid., pp. 798-800.
History