Introduction
I really like WCF as technical framework, it simplifies creating communication layer, but I don't like WCF's design style.
I think creating new method for every data transfer object is poor, so I tried to solve this one.
By nature, WCF is method based web service and has some limitations:
- Doesn't support method overloads
- Doesn't have universal API
- Service Contract depends on business requirement
- Versioning should be on
DataContract
and method levels, operation name should be universal - Other non-.NET clients have to create as many clients as services you have
I think, RPC (Remote Procedure Call) approach isn't a right way, service should be reusable and impact of requirements should be minimal.
As for me, remote API have to satisfy following criteria:
- Stable
and Universal interface
- Transfer data according DTO pattern
Message based service solves major WCF limitations by adding message abstraction.
Spoiler
After reading the article, you'll know how to build reusable message based SOAP service, SOAP client and stop creating new WCF services.
Web service design
Let's take a look at Remote Procedure Call and Message based approaches more deeply.
RPC design
The main idea of RPC style is to share methods, so client works with remote service like with local object. WCF's
ServiceContract
specifies Operations which are available on client side.
For instance:
[ServiceContract]
public interface IRpcService
{
[OperationContract]
void RegisterClient(Client client);
[OperationContract]
Client GetClientByName(string clientName);
[OperationContract]
List<Client> GetAllClients();
}
The service contract is very simple and contains three operations. We have to change the service/client after any changes in the service contract (for example, adding or removing operation, updating operation signature). A real application can have more than 10 operations, so maintaining the service and client is really difficult.
Message based design
The main ideas of Message based design - are Martin Fowler's patterns Data transfer object and Gateway. DTO contains all required data for the communication and Gateway isolate application from communication process. So service based on message design receives a Request message and returns a Response message. Example from Amazon's API.
Example Request
https:
Domain=vpc
&AUTHPARAMS
Example Response
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-02-01/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>198.51.100.1</publicIp>
<domain>vpc</domain>
<allocationId>eipalloc-5723d13e</allocationId>
</AllocateAddressResponse>
So, service contract should look like:
public interface IMessageBasedService
{
Response Execute(Request request);
}
where Request
and Response
could be any DTO, i.e., with single method we can replace any RPC service contract, but WCF uses RPC style.
Message Based Style
As you know, for message based service we could use Request
and Response
objects for transfer any DTOs. But WCF doesn't support this kind of design. All internal WCF's communication based on Message class, i.e., WCF converts any DTO to Message
and sends the Message
from client to server. So, we should use Message
class as Request
and Response
objects.
Following service contract describes communication with and without Response
object:
[ServiceContract]
public interface ISoapService
{
[OperationContract(Action = ServiceMetadata.Action.ProcessOneWay)]
void ProcessOneWay(Message message);
[OperationContract(Action = ServiceMetadata.Action.Process,
ReplyAction = ServiceMetadata.Action.ProcessResponse)]
Message Process(Message message);
}
The ISoapService
is flexible and allows transfer of any data, but it's not enough. We want create, delete objects and execute methods on this one. As for me, the best choice is CRUD operations on object (create, read, update, delete), so we could implement any operation. First of all, let's create SoapServiceClient
, which is able to send and receive any DTO.
Soap service client
The SoapServiceClient
will illustrate how to create the Message
from DTO. The SoapServiceClient
is a wrapper which convert any DTO to a Message
and sends this one to service. The sending Message
should contain following data:
- DTO
- DTO's type, is required for DTO deserialization on the server side
- Target method, will be invoked on the server side
Our goal is to create reusable soap service client which will be able to send/receive any Request/Response
and execute any target operations. As mentioned before, CRUD operations are the best choice, so the client could look like:
var client = new SoapServiceClient("NeliburSoapService");
ClientResponse response = client.Post<ClientResponse>(createRequest);
response = client.Put<ClientResponse>(updateRequest);
Here is the full SoapServiceClient's Post
method, please note on
CreateMessage
method and how concrete DTO type and target method were added through
contentTypeHeader
and actionHeader
.
public TResponse Post<TResponse>(object request)
{
return Send<TResponse>(request, OperationTypeHeader.Post);
}
private TResponse Send<TResponse>(object request, MessageHeader operationType)
{
using (var factory = new ChannelFactory<ISoapService>(_endpointConfigurationName))
{
MessageVersion messageVersion = factory.Endpoint.Binding.MessageVersion;
Message message = CreateMessage(request, operationType, messageVersion);
ISoapService channel = factory.CreateChannel();
Message result = channel.Process(message);
return result.GetBody<TResponse>();
}
}
private static Message CreateMessage(
object request, MessageHeader actionHeader, MessageVersion messageVersion)
{
Message message = Message.CreateMessage(
messageVersion, ServiceMetadata.Operations.Process, request);
var contentTypeHeader = new ContentTypeHeader(request.GetType());
message.Headers.Add(contentTypeHeader);
message.Headers.Add(actionHeader);
return message;
}
SoapContentTypeHeader
and SoapOperationTypeHeader
are almost identical. The
SoapContentTypeHeader
is used for DTO's type transfer and SoapOperationTypeHeader
for transferring target operation. There's not much to speak of all for the MessageHeader
, here is full SoapContentTypeHeader's
code.
internal sealed class SoapContentTypeHeader : MessageHeader
{
private const string NameValue = "nelibur-content-type";
private const string NamespaceValue = "http://nelibur.org/" + NameValue;
private readonly string _contentType;
public SoapContentTypeHeader(Type contentType)
{
_contentType = contentType.Name;
}
public override string Name
{
get { return NameValue; }
}
public override string Namespace
{
get { return NamespaceValue; }
}
public static string ReadHeader(Message request)
{
int headerPosition = request.Headers.FindHeader(NameValue, NamespaceValue);
if (headerPosition == -1)
{
return null;
}
var content = request.Headers.GetHeader<string>(headerPosition);
return content;
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteString(_contentType);
}
}
Here're all SoapServiceClient
's methods:
public static TResponse Get<TResponse>(object request)
public static Task<TResponse> GetAsync<TResponse>(object request)
public static void Post(object request)
public static Task PostAsync(object request)
public static TResponse Post<TResponse>(object request)
public static Task<TResponse> PostAsync<TResponse>(object request)
public static void Put(object request)
public static Task PutAsync(object request)
public static TResponse Put<TResponse>(object request)
public static Task<TResponse> PutAsync<TResponse>(object request)
public static void Delete(object request)
public static Task DeleteAsync(object request)
As you note all CRUD operations have async
version.
Soap service
SoapService should be able to do the following:
- Create concrete
Request
from Message
- Invoke target message by
Request
- Create
Message
from Response
and return this one if required
Our goal is to create something that will invoke appropriate CRUD method by concrete Request
, this example illustrates how to add and get a client:
public sealed class ClientProcessor : IPut<CreateClientRequest>,
IGet<GetClientRequest>
{
private readonly List<Client> _clients = new List<Client>();
public object Get(GetClientRequest request)
{
Client client = _clients.Single(x => x.Id == request.Id);
return new ClientResponse {Id = client.Id, Name = client.Name};
}
public object Put(CreateClientRequest request)
{
var client = new Client
{
Id = Guid.NewGuid(),
Name = request.Name
};
_clients.Add(client);
return new ClientResponse {Id = client.Id};
}
}
The most interesting things: IGet
and IPost
interfaces. These interfaces represent CRUD operations, here is a class diagram. Note the difference between I<Operation>
and I<Operation>OneWay
. For example, IPost
and IPostOneWay
, IPostOneWay
returns void
.
Now, all we need to do is bind a Request
with the appropriate CRUD operation, the simplest way is to bind a Request
with the request processor. NeliburService
is responsible for this functionality. Well, it's as shown below:
public abstract class NeliburService
{
internal static readonly RequestMetadataMap _requests = new RequestMetadataMap();
protected static readonly Configuration _configuration = new Configuration();
private static readonly RequestProcessorMap _requestProcessors = new RequestProcessorMap();
protected static void ProcessOneWay(RequestMetadata requestMetaData)
{
IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
processor.ProcessOneWay(requestMetaData);
}
protected static Message Process(RequestMetadata requestMetaData)
{
IRequestProcessor processor = _requestProcessors.Get(requestMetaData.Type);
return processor.Process(requestMetaData);
}
protected sealed class Configuration : IConfiguration
{
public void Bind<TRequest, TProcessor>(Func<TProcessor> creator)
where TRequest : class
where TProcessor : IRequestOperation
{
if (creator == null)
{
throw Error.ArgumentNull("creator");
}
_requestProcessors.Add<TRequest, TProcessor>(creator);
_requests.Add<TRequest>();
}
public void Bind<TRequest, TProcessor>()
where TRequest : class
where TProcessor : IRequestOperation, new()
{
Bind<TRequest, TProcessor>(() => new TProcessor());
}
}
}
Concrete NeliburSoapService
has only processing and configuration methods, we'll see it later.
RequestMetadataMap
is used for storing Request
's type which is required for creating a concrete Request
from a Message
.
internal sealed class RequestMetadataMap
{
private readonly Dictionary<string, Type> _requestTypes =
new Dictionary<string, Type>();
internal void Add<TRequest>()
where TRequest : class
{
Type requestType = typeof(TRequest);
_requestTypes[requestType.Name] = requestType;
}
internal RequestMetadata FromRestMessage(Message message)
{
UriTemplateMatch templateMatch = WebOperationContext.Current.IncomingRequest.UriTemplateMatch;
NameValueCollection queryParams = templateMatch.QueryParameters;
string typeName = UrlSerializer.FromQueryParams(queryParams).GetTypeValue();
Type targetType = GetRequestType(typeName);
return RequestMetadata.FromRestMessage(message, targetType);
}
internal RequestMetadata FromSoapMessage(Message message)
{
string typeName = SoapContentTypeHeader.ReadHeader(message);
Type targetType = GetRequestType(typeName);
return RequestMetadata.FromSoapMessage(message, targetType);
}
private Type GetRequestType(string typeName)
{
Type result;
if (_requestTypes.TryGetValue(typeName, out result))
{
return result;
}
string errorMessage = string.Format(
"Binding on {0} is absent. Use the Bind method on an appropriate NeliburService", typeName);
throw Error.InvalidOperation(errorMessage);
}
}
RequestProcessorMap
binds Request
's type with request processor.
internal sealed class RequestProcessorMap
{
private readonly Dictionary<Type, IRequestProcessor> _repository =
new Dictionary<Type, IRequestProcessor>();
public void Add<TRequest, TProcessor>(Func<TProcessor> creator)
where TRequest : class
where TProcessor : IRequestOperation
{
Type requestType = typeof(TRequest);
IRequestProcessor context = new RequestProcessor<TRequest, TProcessor>(creator);
_repository[requestType] = context;
}
public IRequestProcessor Get(Type requestType)
{
return _repository[requestType];
}
}
Now we're ready for the last step - invoke target method. Here is our SoapService
, the same as usual WCF service.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapService : ISoapService
{
public Message Process(Message message)
{
return NeliburSoapService.Process(message);
}
public void ProcessOneWay(Message message)
{
NeliburSoapService.ProcessOneWay(message);
}
}
First of all, let's take a look on the sequence diagram, the diagram describes execution process on the service side.
Let's dive into code step by step. NeliburSoapService
just executes another code, take a look.
public sealed class NeliburSoapService : NeliburService
{
private NeliburSoapService()
{
}
public static IConfiguration Configure(Action<IConfiguration> action)
{
action(_configuration);
return _configuration;
}
public static Message Process(Message message)
{
RequestMetadata metadata = _requests.FromSoapMessage(message);
return Process(metadata);
}
public static void ProcessOneWay(Message message)
{
RequestMetadata metadata = _requests.FromSoapMessage(message);
ProcessOneWay(metadata);
}
}
NeliburSoapService
just decorates RequestMetadataMap
i.e. calls appropriate methods for creating RequestMetadata
for soap message
The most interesting thing happens here:
RequestMetadata metadata = _requests.FromSoapMessage(message)
Process(metadata)
.
SoapRequestMetadata
is the main object which accumulate: CRUD operation type, Request's data, its type and could create a response message.
internal sealed class SoapRequestMetadata : RequestMetadata
{
private readonly MessageVersion _messageVersion;
private readonly object _request;
internal SoapRequestMetadata(Message message, Type targetType) : base(targetType)
{
_messageVersion = message.Version;
_request = CreateRequest(message, targetType);
OperationType = SoapOperationTypeHeader.ReadHeader(message);
}
public override string OperationType { get; protected set; }
public override Message CreateResponse(object response)
{
return Message.CreateMessage(_messageVersion, SoapServiceMetadata.Action.ProcessResponse, response);
}
public override TRequest GetRequest<TRequest>()
{
return (TRequest)_request;
}
private static object CreateRequest(Message message, Type targetType)
{
using (XmlDictionaryReader reader = message.GetReaderAtBodyContents())
{
var serializer = new DataContractSerializer(targetType);
return serializer.ReadObject(reader);
}
}
}
At the end, we just call appropriate CRUD operation thru the RequestProcessor
. The RequestProcessor
uses
RequestMetadata
to determine an operation and calls this one then returns result to the SoapServiceClient
.
Here's the implementation:
internal sealed class RequestProcessor<TRequest, TProcessor> : IRequestProcessor
where TRequest : class
where TProcessor : IRequestOperation
{
private readonly Func<TProcessor> _creator;
public RequestProcessor(Func<TProcessor> creator)
{
_creator = creator;
}
public Message Process(RequestMetadata metadata)
{
switch (metadata.OperationType)
{
case OperationType.Get:
return Get(metadata);
case OperationType.Post:
return Post(metadata);
case OperationType.Put:
return Put(metadata);
case OperationType.Delete:
return Delete(metadata);
default:
string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
throw Error.InvalidOperation(message);
}
}
public void ProcessOneWay(RequestMetadata metadata)
{
switch (metadata.OperationType)
{
case OperationType.Get:
GetOneWay(metadata);
break;
case OperationType.Post:
PostOneWay(metadata);
break;
case OperationType.Put:
PutOneWay(metadata);
break;
case OperationType.Delete:
DeleteOneWay(metadata);
break;
default:
string message = string.Format("Invalid operation type: {0}", metadata.OperationType);
throw Error.InvalidOperation(message);
}
}
private Message Delete(RequestMetadata metadata)
{
var service = (IDelete<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Delete(request);
return metadata.CreateResponse(result);
}
private void DeleteOneWay(RequestMetadata metadata)
{
var service = (IDeleteOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.DeleteOneWay(request);
}
private Message Get(RequestMetadata metadata)
{
var service = (IGet<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Get(request);
return metadata.CreateResponse(result);
}
private void GetOneWay(RequestMetadata metadata)
{
var service = (IGetOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.GetOneWay(request);
}
private Message Post(RequestMetadata metadata)
{
var service = (IPost<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Post(request);
return metadata.CreateResponse(result);
}
private void PostOneWay(RequestMetadata metadata)
{
var service = (IPostOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.PostOneWay(request);
}
private Message Put(RequestMetadata metadata)
{
var service = (IPut<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
object result = service.Put(request);
return metadata.CreateResponse(result);
}
private void PutOneWay(RequestMetadata metadata)
{
var service = (IPutOneWay<TRequest>)_creator();
var request = metadata.GetRequest<TRequest>();
service.PutOneWay(request);
}
}
Demo Sample
Well, the theoretical part is over. Let's take a look at the practical side. The demo shows how to register client, update some information and get the client.
First of all, we declare data contracts:
CreateClientRequest
- request for creating a new client-
UpdateClientRequest
- request for updating client's email GetClientRequest
- request for get client by Id-
ClientResponse
- client's info -
RemoveClientRequest
- removal request
Server's side
Config file is the same as usual.
<configuration>
<system.serviceModel>
<services>
<service name="Nelibur.ServiceModel.Services.Default.SoapServicePerCall">
<endpoint address="http://localhost:5060/service" binding="basicHttpBinding"
bindingConfiguration="ServiceBinding"
contract="Nelibur.ServiceModel.Contracts.ISoapService" />
</service>
</services>
<bindings>
<basicHttpBinding>
<binding name="ServiceBinding">
<security mode="None">
<transport clientCredentialType="None" />
</security>
</binding>
</basicHttpBinding>
</bindings>
</system.serviceModel>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
Nelibur already contains default SoapServicePerCall
service, here is the implementation:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public sealed class SoapServicePerCall : ISoapService
{
public Message Process(Message message)
{
return NeliburSoapService.Process(message);
}
public void ProcessOneWay(Message message)
{
NeliburSoapService.ProcessOneWay(message);
}
}
Bind all requests with request processor. I've created only one request processor for simplicity. You can create as many request processors as you want.
Take a look at Martin Fowler's article about CQRS, it'll help you to make the right choice.
Here's the binding:
private static void BindRequestToProcessors()
{
NeliburSoapService.Configure(x =>
{
x.Bind<CreateClientRequest, ClientProcessor>();
x.Bind<UpdateClientRequest, ClientProcessor>();
x.Bind<DeleteClientRequest, ClientProcessor>();
x.Bind<GetClientRequest, ClientProcessor>();
});
}
and finally the ClientProcessor
:
public sealed class ClientProcessor : IPost<CreateClientRequest>,
IGet<GetClientRequest>,
IDeleteOneWay<DeleteClientRequest>,
IPut<UpdateClientRequest>
{
private static List<Client> _clients = new List<Client>();
public void DeleteOneWay(DeleteClientRequest request)
{
Console.WriteLine("Delete Request: {0}\n", request);
_clients = _clients.Where(x => x.Id != request.Id).ToList();
}
public object Get(GetClientRequest request)
{
Console.WriteLine("Get Request: {0}", request);
Client client = _clients.Single(x => x.Id == request.Id);
return new ClientResponse { Id = client.Id, Email = client.Email };
}
public object Post(CreateClientRequest request)
{
Console.WriteLine("Post Request: {0}", request);
var client = new Client
{
Id = Guid.NewGuid(),
Email = request.Email
};
_clients.Add(client);
return new ClientResponse { Id = client.Id, Email = client.Email };
}
public object Put(UpdateClientRequest request)
{
Console.WriteLine("Put Request: {0}", request);
Client client = _clients.Single(x => x.Id == request.Id);
client.Email = request.Email;
return new ClientResponse { Id = client.Id, Email = client.Email };
}
}
Client's side
Client's code is as simple as possible:
private static void Main()
{
var client = new SoapServiceClient("NeliburSoapService");
var createRequest = new CreateClientRequest
{
Email = "email@email.com"
};
Console.WriteLine("POST Request: {0}", createRequest);
ClientResponse response = client.Post<ClientResponse>(createRequest);
Console.WriteLine("POST Response: {0}\n", response);
var updateRequest = new UpdateClientRequest
{
Email = "new@email.com",
Id = response.Id
};
Console.WriteLine("PUT Request: {0}", updateRequest);
response = client.Put<ClientResponse>(updateRequest);
Console.WriteLine("PUT Response: {0}\n", response);
var getClientRequest = new GetClientRequest
{
Id = response.Id
};
Console.WriteLine("GET Request: {0}", getClientRequest);
response = client.Get<ClientResponse>(getClientRequest);
Console.WriteLine("GET Response: {0}\n", response);
var deleteRequest = new DeleteClientRequest
{
Id = response.Id
};
Console.WriteLine("DELETE Request: {0}", deleteRequest);
client.Delete(deleteRequest);
Console.ReadKey();
}
and execution results:
Here's the Service Client:
and soap service:
That's all folks
I hope you enjoyed it, please take the time to post a comment. Here's you can know how to build RESTful Message Based Web Services with WCF. Thanks for reading the article.
History
- 12 Aug 2013
- 17 May 2014
- Changed client's methods signature. Example:
client.Get<GetClientRequest, ClientResponse>(request)
=> client.Get<ClientResponse>(request)