WCF Intermediate Service between client and server

Introduction:

The purpose of this article is to create a WCF service which can work as load balancer or router.Hosting a WCF service requires creating the service contract, Data contract and configure end points, host the service, generate the Web Service Description Language(WSDL), enable metadata exchange point so that client can create the proxy. Both the client and server channel should be agree on addressing, binding and message filtering. Sometimes the product requires some additional features like logging, routing, load balancing or introducing a security boundary. We can easily introduce intermediate service and achieve these kind of features by some tweaking in addressing and filtering behavior.

Using the Code:

The attached source contain following projects.

  1. TargetServiceContract: contract for the target WCF service.
  2. TargetHost: Host console application for target WCF Service.
  3. IntermediateServiceContract: Service contract which can work as load balancer or router.
  4. IntermediateServiceHost: Host console application which host the IntermediateServiceContract.
  5. Client: it is the client project which consumes the WCF target service.

Using the operation type in an operation signature doesn’t by definition mean that any message can be processed by the service. By default Action header is used to target the particular operation. The service model inspects incoming message and looks for an operation on the endpoint that has a matching action header. If it can’t find a match, the request is rejected.

When would action headers not match the operation being invoked? One possibility is when an intermediary or routing service is placed between the client and the application service. In this scenario, the client application doesn’t know of the presence of the intermediate service and the action header targets the application service.

Action header indicates the URI for the operation to be invoked. It is generated by concatenating the service contract namespace with the service contract friendly name and the operation name.

https://Example/IntermediateService/TargetServiceContract/VerifyMessage

The reply action appends “Response” to this.

https://Example/IntermediateService/TargetServiceContract/VerifyMessageResponse

Client can generate the same Action, ReplyAction and proxy setting by generating the service proxy. Here is the code which is generated from the proxy.

[System.ServiceModel.OperationContractAttribute(
    Action="https://Example/IntermediateService/TargetServiceContract/VerifyMessage",
    ReplyAction="https://Example/IntermediateService/TargetServiceContract/VerifyMessageResponse")]
 string VerifyMessage(string message);

The client binding configuration will reflect physical address. Here is the client endpoint setting which have the physical address as the address of the target service. We can generate it by creating the service reference by using the metadata endpoint("https://localhost:8001/TargetService/Mex").

    <endpoint address="https://localhost:8001/TargetService"
                      binding="wsHttpBinding"
                      bindingConfiguration="wsHttpEndPoint"
                      contract="TargetServiceClient.TargetServiceContract"
                      name="wsHttpEndPoint"
                      behaviorConfiguration="clientBehavior" />

When client send any message to service, message is sent to physical address with action mentioned in the generated proxy file.

Intermediate Service:

Intermediate service is designed to receive messages that can target any service and it must be able to forward the original message to appropriate service without any change in the body of the message. The intermediate service may only look at only the message headers but the original message elements should be forwarded without any change.

    [OperationContract(Name="ProcessMessage",Action="*",ReplyAction="*")]
    Message ProcessMessage(Message message);

 

In case if the Action and ReplyAction is * then the all the messages are dispatched to the same operation and only one operation can have Action and ReplyAction as * in the service contract. Since Channel dispatcher always gets the input argument as Message type(defined in System.ServiceModel.Channels) and return the value as Message type so the intermediate service should have operation which has Input argument and return type as Message. This operation can work as target of any request.

Forwarding Message to original service:

Service proxy usually has strongly typed operation but in intermediate service we use a generic proxy operation which can send any type as input argument and receive any type as response. Since the contract has untyped message, the same message can be sent to the service and the response can be directly sent to the client. Intermediate service doesn't do any other change except changing the To address to send the message to the appropriate service.

Sending the request to Intermediate service:

When and intermediate service is introduced, it is always best if the client can send messages using the correct To header for the service while still sending the message to the intermediate service. The first way to achieve this is to configure ClientViaBehavior as mentioned bellow. This tells the client to generate the message which would have the To header as the end point service address but it would go via the intermediate service.

<client>
            <endpoint address="https://localhost:8002/IntermediateServiceExample"
                      binding="wsHttpBinding"
                      bindingConfiguration="wsHttpEndPoint"
                      contract="TargetServiceClient.TargetServiceContract"
                      name="wsHttpEndPoint" behaviorConfiguration="clientBehavior">
            </endpoint>
        </client>
      <behaviors>
        <endpointBehaviors>
          <behavior name="clientBehavior">
            <clientVia viaUri="https://localhost:8002/IntermediateServiceExample%22/>
          </behavior>
        </endpointBehaviors>
      </behaviors>

The another way to achieve this to mention the listenuri in the target service so that the logical address of the target service can be the address of intermediate service and physical address can be the endpoint address. In this case when the client generates the proxy, it would have the address as the address of the intermediate service.

<endpoint address="https://localhost:8002/IntermediateServiceExample"
          binding="wsHttpBinding"
          bindingConfiguration="wsHttpBinding" name="wsHttpEndPoint"
          contract="IntermediateServiceExample.ITargetServiceContract"
          listenUri="https://localhost:8001/TargetService" />

In this scenario the service must be aware of the intermediate service which I think is wrong so it is better to tell the intermediate service address to the client and client can use the first way. The client sends the message to intermediate service with To header as the target service address. Thus, the To header will never match the router's logical address. By default, services use the EndpointAddressMessageFilter to determine whether a message's To header matches any of its configured endpoints. Since a router can't expect this to work, it should install the MatchAllMessageFilter. We can achieve this by using the following filter. It passes all the to header messages.

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
                     AddressFilterMode = AddressFilterMode.Any)]
    public class IntermediateServiceManager : IIntermediateServiceContract

Now when the client sends any message, it would go via the intermediate service. The intermediate service can load balance it or do the routing, send the request to appropiate service,get the response and return the same response to the client. Since all the requests from the client and all the response from the target service would go via the intermediate service, we can do logging, tracing for each and every message going through the channel.

WCFIntermediateService_src.zip