Introduction
This article describes a framework for a static-interface WCF
service.
At some point or another, everyone who has created a WCF
service that is being consumed by a 3rd party is going to have updates to that
service that break the compatibility between client and service. At the very least they will have updates to
the data that is flowing through that service, if not the contract itself. When a WCF contract changes, or when the data
flowing through that contract changes, you typically have to rebuild the client
and service to include any of the changes.
What I wanted to do with this code sample is to come up with
a way to define a WCF interface that would not require changes when the content
flowing through it changed, nor when new methods are added. What I came up with is a WCF contract that
defines the bare minimum of data required to identify messages flowing through
it- this means that the underlying data can change as often as required,
without changes to the WCF interface itself. Both the client and service can define the
level of support that they want to maintain in regards to the overall data,
independent of the WCF contract.
Let’s begin with an example of a typical WCF contract where
I am passing a Person
class in, and getting an Address
class back:
[OperationContract]
Address GetAddress(Person person);
This is all well and good, and works fine, but what happens
when I want to change my service, and get an Address
back by passing a
Customer
in? I now have to either change the interface
so that it accepts a
Customer
, or add a whole new interface that supports the
new call:
[OperationContract]
Address GetAddress(Customer customer);
Adding a new interface is the best option, as it doesn't break the compatibility of the WCF contract, however it does still require the
client and service to recompile against this updated contract to support the
new method.
Note: Changing the
name or data type of a member, or removing data members is a breaking change. Likewise changing the type, name, or
parameters of a method is a breaking change.
Breaking changes will require an updated contract on the client and
service.
So, you can see that I would have to add a new
GetAddress
method that takes a Customer
object instead of just changing the
existing one if I want to maintain compatibility with any of the clients
consuming my service. In the case of
this example, this is not an optimal solution, as I don’t want the interface to
change when I make ANY changes to my underlying service, including adding a new
method.
Solution
The solution to this problem is to step away from the world
of simple well-defined WCF calls, and into the shady world of custom serialization,
message handlers, and other scary stuff.
Not really… actually, it’s quite simple if you use the
framework that I’m laying out here.
The solution that I put together for this example makes some
fundamental changes to the WCF contract that we saw above, as well as requiring
some changes outside of the WCF calls.
First off, the interface that we were using has to go. In its place, we have two new methods, which will
look like this:
[OperationContract(IsOneWay = true)]
void SendMessageOneWay(int topicId, int messageId, byte[] message);
[OperationContract]
byte[] SendMessageRequestResponse(int topicId, int messageId, byte[] message);
Here you can see that we’ve replaced all of the previous
GetAddress
methods with two generic looking methods. Each method takes three parameters: a topicId
,
messageId
, and a message
.
Note: Both topicId
and
messageId
are integers that map to an enumeration value internally. They are created as integers to support the addition
or removal of values within the internal enumeration without breaking
compatibility.
topicId
: The top-level category of the message being
sent. This serves as a means to organize
messages into meaningful groups. For the
purpose of this example, let’s assume that we are using a topic of “Address”.
messageId
: The
specific message within the topic that was specified. This identifies the message that will be sent
through this method for the purpose of routing it to the correct handler (more
on that later). Message IDs can represent
the different messages we want to send, so in this example let’s assume we have
two message IDs: “GetAddressFromPerson
” and “GetAddressFromCustomer
”.
message
: A serialized
representation of whatever message is being sent over WCF. (I will cover this later in the Protocol
Buffers section)
return value
(SendMessageRequestResponse
only): A serialized representation of the response
message.
For this example, the TopicId
and MessageId
enumerations
contain the following:
public enum TopicId : int
{
Address = 1,
Person = 2
}
public enum MessageId : int
{
GetAddressFromPerson = 1,
GetAddressFromCustomer = 2,
GetAllAddresses = 3
}
Note on enumeration types in this context: You
might be asking why enumerations are used on either end of the service, but the
interface specifies an int for the topicId
, and messageId
parameters. The answer is again related to compatibility…
if we decouple the method’s interface from a specific type (such as TopicId
),
we can support backward and forward compatibility on the interface when that
type changes. So, if the service receives
a message that doesn’t have a topic or message ID that it knows about, it can
simply discard it, or throw an error.
Protocol Buffers
As you’ve no doubt noticed, the “message
” parameter in the
operation contracts (and the return value on the “SendMessageRequestResponse
” method) is an array of
bytes. This array of bytes represents a
serialized .NET object, which is being sent or received through WCF.
In order to make the WCF interface generic and extensible,
the objects that we pass back and forth need to be made into some generic
format that does not impact the WCF contract when something is added, removed,
or changed. For this example, I am using
a protocol buffer that has been serialized to a byte array, but you could use
pretty much anything that can be sent over WCF.
What does this mean? First
of all, it means that in order to use this project, you’ll need to add a
reference to protobuf-net (I used NuGet to add the reference- since it’s now
integrated into VS2012!). You’ll also
need to create protobuf objects to send back and forth. For example, my Person
class looks like this:
[ProtoContract]
public class Address
{
[ProtoMember(1)]
public string Address1 { get; set; }
[ProtoMember(2)]
public string Address2 { get; set; }
[ProtoMember(3)]
public string City { get; set; }
[ProtoMember(4)]
public string State { get; set; }
}
Protocol buffers are very efficient, in both CPU
and memory usage, which is why I chose to use it as the serialization engine
for this sample. You can read all about
protobuf-net here: http://code.google.com/p/protobuf-net/
Using the code
In this sample, I have included a couple of helper classes
designed to ease the pain of moving to a solution such as this. Note that these are only a few of the classes contained within the contracts project.
- IWcfContract.cs
This is the interface that contains the “SendMessageOneWay
” and “SendMessageRequestResponse
”
methods for the WCF service and client. This file also contains the callback version of the contract. Note that the names and namespaces have been explicitly set on the contracts contained within so that they are seen as interchangeable by WCF (IWcfContract
and IWcfContractWithCallback
).
- SerializationHelper.cs
This is a static class that has two methods that allow the serialization or deserialization of an object.
- ServiceWrapperBase.cs
This abstract class is meant to be inherited by a class that acts as the service proxy for the WCF service. It implements the WCF methods
“SendMessageOneWay
” and “SendMessageRequestResponse
”, and provides support for registering handlers for topic and message IDs.
- ClientWrapper.cs
This class takes a reference to the WCF service, and wraps the “SendMessageOneWay
”
and “SendMessageRequestResponse
” methods, and exposes them in a friendlier manner as one “Send
” method.
- ClientWrapperBuilder.cs
This class is a static factory class used to create the above mentioned ClientWrapper
class.
Setting up the service
To set up the service, you’ll create a class like you would for any WCF service, by decorating it with the ServiceBehavior
attribute, like so:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class MyService
{…}
Next, make your class implement the ServiceWrapperBase
, as
follows:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class MyService : ServiceWrapperBase<TopicId, MessageId>
{
public MyService(bool isDuplex)
: base(isDuplex)
{ ... }
}
Note: The TopicId
and
MessageId
types should be enumeration types that have an underlying type of “int”
(see my comments above related to this).
Now that you have a class set up that implements the
ServiceWrapperBase
, it’s time to get that class hosted as a WCF service using
the following code:
MyService service = new MyService();
ServiceHost serviceHost = new ServiceHost(service);
serviceHost.Open();
Remember: You need to
add a reference to System.ServiceModel
in order to have the ServiceHost
class
available.
Once you’ve done this, and say put the code into a console
application, you will have a self-hosted WCF service.
Registering message
handlers
After you have the code in place to host your service, let’s
register some message handlers!
Registering message handlers is how the ServiceWrapperBase
class routes
incoming messages to the appropriate methods.
For example, if I wanted to receive a message with a TopicId
of Address
,
a MessageId
of GetAddressFromCustomer
, and a return value of Person
, I would
register a handler as follows:
First I would create a method to handle the message:
private Address HandleGetAddressFromCustomer(Customer requestMessage)
{…}
Then I would register the handler method with the
ServiceWrapperBase
:
RegisterHandler<Customer, Address>(TopicId.Address, MessageId.GetAddressFromCustomer, HandleGetAddressFromCustomer);
At this point, anything that comes into the WCF service with
the above TopicId
and MessageId
will be routed to the HandleGetAddressFromCustomer
method, in which
I would do what I like with the incoming Customer
data, returning an
Address
object.
Calling the service
Calling the service is about as simple as it gets… you’ll need a ChannelFactory
to create an instance of IWcfContract
, which I will leave
as an exercise for the reader. you'll just need to call the appropriate GetClientWrapper factory method on the ClientWrapperBuilder class.
You can create a new instance of the ClientWrapper
class by doing the
following:
clientWrapper = ClientWrapperBuilder.GetDuplexClientWrapper<TopicId, MessageId, IWcfContractWithCallback>("ProtobufSpikeCb");
clientWrapper = ClientWrapperBuilder.GetSimplexClientWrapper<TopicId, MessageId, IWcfContract>("ProtobufSpike");
Note that I am instantiating the ClientWrapper
class with
three types: TopicId
, MessageId
, and IWcfContract
(or alternatively the IWcfContractWithCallback
for duplex) The TopicId
and MessageId
serve the same
purpose as they do on the service side, and the IWcfContract
is specifying the type
of the service that you are passing in.
Note: the
IWcfContract
type that is passed into the ClientWrapperBuilder
is constrained within the ClientWrapperBuilder
class to only accept types
implementing IWcfContract
or IWcfContractWithCallback.IWcfContractWithCallback
.
Once you have an instance of the ClientWrapper
, you can call
it like this:
Address response = clientWrapper.Send<Address, Customer>(TopicId.Address, MessageId.GetAddressFromCustomer, new Customer() { Id = "123" });
Note: The above example is for calling the ClientWrapper
to
send a request/response type of message.
If you want to send a one-way message, simply call the ClientWrapper
like so:
clientWrapper.Send<OneWayMessage>(TopicId.Other, MessageId.OneWay, message);\
Duplex - Using the Callback
The following is an example of how to register for a callback (on the client), and how to call a callback (on the service).
<p />
if (clientWrapper.IsDuplex)
clientWrapper.RegisterCallbackHandler<string>(TopicId.Other, MessageId.PrintData, PrintData);
As you can see above, the ClientWrapper
is called to register a callback handler (in this case the callback handler takes one parameter, a string
). The registration method for callbacks is the same as the registration method for the service handlers.
Calling the callback from the service side is as simple as doing the following inside of your service, which should implement the ServiceWrapperBase
class (giving you access to the isDuplex
and Callback
members).
if (isDuplex)
Callback<string>(TopicId.Other, MessageId.PrintData, "Hello there! This is a callback!", OperationContext.Current);
Other Comments
While the generic interface described here defeats the
purpose of WCF’s service versioning and compatibility scheme, it has a place in
the software that I am working on, and is better than some of the alternatives I’ve
looked into. The real benefit here is
that I get a stable and unchanging interface, along with the perks of using
WCF. Perks such as security, reliable
transport, and whatever else I’d like to utilize from the WCF stack outside of
contracts.
Points of Interest
Bonus! You can use
this code to create a WCF service that has a set of features that change
dynamically at runtime. You can register
handlers for methods when making them available, and then unregister them when
you don’t want them to be available- all without stopping your application, and
certainly without recompiling anything.
History
14NOV2012 - First revision.
11DEC2012 - Second revision: Added functionality to support duplex WCF channels, as well as certificate-based security for the client. Refactored the client wrapper with better creation methods, simplified code all-around.