The very common requirement is to route WCF messages from the publicly exposed front-end service (perimeter network) to other services sitting within the local network. This is the good security model that lets us implement routing or load balancer if needed. The front-end service will also act as a firewall in this scenario. The routing may be performed based on the requested service method, content or custom algorithm.
The image below shows typical hight level service infrastructure within the organization.
Let’s start with defining our service that the client will use to send requests. The “Calculate” method will be used to perform time consuming operation that will require more CPU usage.
[ServiceContract]
public interface IMessageService
{
[OperationContract]
string SendMessage(string value);
[OperationContract]
int Calculate(string value);
}
public class MessageService : IMessageService
{
public string SendMessage(string value)
{
return string.Format("You have sent: {0}", value);
}
public int Calculate(string value)
{
//do calculation
return 999;
}
}
Next, we need to define IRouter interface. In our example we will use two methods: RouteMessage, that will process and route the messages and AddAvailableEndPoints, that will be used to assign available endpoints for the requests to be routed to. Please note that our router will not be using service contracts which is very useful as we don’t want to recompile router service each time our message service interface changes. You may also implement function to assign routing rules, in our example we will hard-code the rules for better clarity.
[ServiceContract]
public interface IRouter
{
[OperationContract(Action = "*", ReplyAction = "*")]
Message RouteMessage(Message message);
void AddAvailableEndPoints(List<EndpointAddress> addressList);
}
Let’s create our router class now. Please note that we are using static constructor as we will only use one instance of the service (creating and destroying channel factory is very costly operation).
static IChannelFactory<IRequestChannel> factory;
List<EndpointAddress> addressList;
int routeCount;
//create only one instance
static Router()
{
try
{
var binding = new BasicHttpBinding();
factory = binding.BuildChannelFactory<IRequestChannel>(binding);
factory.Open();
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.Message);
}
}
Now we need to implement the actual routing algorithm. In our case, when message arrives we will check the action method that proxy object will use to execute on the target service. We have hard-coded the method “Calculate” to be routed to service2, the other request will be routed equally among available services. You can also use XmlDocument class to parse message content if needed.
Of course in the production environment you may want to pass in routing rules as a object and examine that when processing arrived message prior routing it to appropriate service.
public Message RouteMessage(Message message)
{
IRequestChannel channel = null;
Console.WriteLine("Action {0}", message.Headers.Action);
try
{
//Route based on custom conditions
if (message.Headers.Action.ToLower().EndsWith("calculate")) //or use custom routing table stored in other location
{
//use second endpoint as it has more resources for time consuming operations
channel = factory.CreateChannel(this.addressList.Skip(1).First());
Console.WriteLine("Routed to: {0}\n", this.addressList.Skip(1).First().Uri);
}
else
{
//or
#region Route other requests equally
//we assume only 2 endpoints for this example
if (routeCount % 2 == 0)
{
channel = factory.CreateChannel(this.addressList.First());
Console.WriteLine("Routed to: {0}\n", this.addressList.First().Uri);
}
else
{
channel = factory.CreateChannel(this.addressList.Skip(1).First());
Console.WriteLine("Routed to: {0}\n", this.addressList.Skip(1).First().Uri);
}
//reset route counter
if (routeCount > 10000) { routeCount = 0; } else { routeCount++; }
#endregion
}
//remove context as the message will be redirected
message.Properties.Remove("ContextMessageProperty");
channel.Open();
var reply = channel.Request(message);
channel.Close();
return reply;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return null;
}
}
At this time, we can create client application that will call our front-end service(router) multiple times for our test.
static void Main(string[] args)
{
Console.Write("Press enter to send multiple messages");
Console.ReadLine();
var baseFrontOfficeAddress = new Uri("https://localhost:81/FrontOffice");
try
{
var binding = new BasicHttpBinding();
var endpoint = new EndpointAddress(baseFrontOfficeAddress);
var factory = new ChannelFactory<IMessageService>(binding, endpoint);
var channel = factory.CreateChannel();
//simulate multiple requests
for (var i = 0; i < 10; i++)
{
var reply = channel.SendMessage("test message");
Console.WriteLine(reply);
var replyCalulated = channel.Calculate("test message");
Console.WriteLine("Calculated value: " + replyCalulated);
}
Console.ReadLine();
factory.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
}
In order to receive client requests we need to start our services. In our example we will host router and available services within the same application. Normally we would deploy router service within DMZ zone and target services on the separate computers withing our local network. For security reasons, only router service would be exposed to public network.
static void Main(string[] args)
{
//execute this first using cmd (win7)
//netsh http add urlacl url=https://+:81/FrontOffice user=Domain(or PC name)\UserName
//netsh http add urlacl url=https://+:81/service1 user=Domain(or PC name)\UserName
//netsh http add urlacl url=https://+:81/service2 user=Domain(or PC name)\UserName
//front office endpoint - all clients will call this address before requests will be routed
var baseFrontOfficeAddress = new Uri("https://localhost:81/FrontOffice");
#region Target service endpoints that requests can be routed to (can be more than 2)
var baseEndPointAddress1 = new Uri("https://localhost:81/service1");
var baseEndPointAddress2 = new Uri("https://localhost:81/service2");
var endPoint1 = new EndpointAddress(baseEndPointAddress1);
var endPoint2 = new EndpointAddress(baseEndPointAddress2);
#endregion
#region These services should normally be deployed to different servers within the organization
//start service 1
var hostService1 = new ServiceHost(typeof(MessageService1), baseEndPointAddress1);
hostService1.AddServiceEndpoint(typeof(IMessageService), new BasicHttpBinding(), "");
hostService1.Open();
Console.WriteLine("MessageService1 service running");
//start service 2
var hostService2 = new ServiceHost(typeof(MessageService2), baseEndPointAddress2);
hostService2.AddServiceEndpoint(typeof(IMessageService), new BasicHttpBinding(), "");
hostService2.Open();
Console.WriteLine("MessageService2 service running");
#endregion
#region Start router service
var router = new Router();
//add available service enpoints
router.AddAvailableEndPoints(new List<EndpointAddress>() { endPoint1, endPoint2 });
var routerHost = new ServiceHost(router);
routerHost.AddServiceEndpoint(typeof(IRouter), new BasicHttpBinding(), baseFrontOfficeAddress);
routerHost.Open();
Console.WriteLine("Router service running");
#endregion
Console.WriteLine("---Run client to send messages---");
Console.ReadLine();
}
The results of our test is shown below. In this example we chose to route all requests for “Caluculate” method to “service2” (as it located on faster machine within the network), the other requests are routed equally to service1 or service2.
Above way of routing messages is very flexible as it is implemented on the very low level. The other way of routing messages is to use RoutingService class. In this approach you would define the routing table, endpoints and filters in your configuration file. Based on these settings you would add service behavior that will use RoutingService class to route messages.
I have attached project files below for your tests. Please execute below commands before testing it:
netsh http add urlacl url=https://+:81/FrontOffice user=Domain(or PC name)\UserName”
netsh http add urlacl url=https://+:81/service1 user=Domain(or PC name)\UserName
netsh http add urlacl url=https://+:81/service2 user=Domain(or PC name)\UserName
WCF_Router