The Local History project: Some challenges

Some time ago I mentioned thinking about a project on Local History. This project is still in a very conceptive application. But I already know that I want to have Windows Service and a Windows UI. More on other details later, but now let's talk about communication between the Windows Service and the Windows UI.

It's possible to do some very basic communication with a Windows Service. This can be done like this:

On the Windows Service handle commands like this:

1 protected override void OnCustomCommand(int command) 2 { 3 base.OnCustomCommand(command); 4 //Do your handling. 5 }

On the Windows UI send the commands like this:

1 ServiceController sc = new ServiceController("WindowsServiceName"); 2 int customCommand = 21; 3 sc.ExecuteCommand(customCommand);

Yes you can see this are just very basic commands, you can send integers to the Windows Service that it needs to handle. I'm sure there are enough samples to find that only needs this basic commands. But I need more, much more.

How can we do more advanced communication between the Windows UI and the Windows Service?

In the old world, .NET 1.1 or .NET 2.0, we would turn to .NET Remoting using Named Pipes. But nowadays we have .NET 3.0 and even .NET 3.5, specially we have Windows Communication Foundation (WCF). I didn't have any opportunity to use WCF. So I thought why not give WCF a try using Named Pipes.

So first thing I did, was forget about the communication channel and model the service contract and data contract.

The Service Contract is still very generic and should probably need some modifications in the future, but so far this is it:

1 [ServiceContract] 2 public interface ILocalHistory 3 { 4 [OperationContract] 5 void Do(ServiceAction action); 6 }

As you can see I make use of "ServiceAction" as data. For those interested "Operation" is a very basic enumeration. For the "ServiceAction" I have a DataContract:

1 [DataContract] 2 public class ServiceAction 3 { 4 [DataMember] 5 public Operation Operation { get; set; } 6 7 [DataMember] 8 public string Path { get; set; } 9 }

The next step was some basic code to host the service:

1 ServiceHost localHistoryServiceHost = new ServiceHost(typeof(LocalHistoryWcfService)); 2 localHistoryServiceHost.Open();

After this I still needed to configure the service:

1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <services> 5 <service name="MM.LocalHistory.Service.LocalHistoryWcfService"> 6 <endpoint address="net.pipe://localhost/LocalHistory/Service" 7 binding="netNamedPipeBinding" 8 contract="MM.LocalHistory.Service.Common.ILocalHistory"/> 9 </service> 10 </services> 11 </system.serviceModel> 12 </configuration>

The very minimum configuration you need to get the service listening to a named pipe is the above. I say very minimum, because a lot of times you might want more that this.

But let me explain something first. As you can see in lines 5 and 8 a Fully Qualified Name is used to specify the service implementer and the service contract. I'm used to also specify the Assembly name, but don't do that, it would not work.

So now we have ourselves a Service providing a Named Pipe service, I thought time for a consumer, a Named Pipe client. As I'm more used to working with Webservices I thought why not add a Service Reference from within Visual Studio 2008. But I learned the hard way, you first have to add a metadata exchange service to your WCF service or else Visual Studio won't be able to generate a proxy, or even discover the service.

But the nicest thing about this is, you only have to change the configuration to add a Metadata Exchange Service (MEX). Although, it took me a while to figure out this was all I needed to do.

1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <services> 5 <service name="MM.LocalHistory.Service.LocalHistoryWcfService" 6 behaviorConfiguration="MetadataSupport"> 7 <endpoint address="net.pipe://localhost/LocalHistory/Service" 8 binding="netNamedPipeBinding" 9 contract="MM.LocalHistory.Service.Common.ILocalHistory"/> 10 <endpoint address="net.pipe://localhost/LocalHistory/Service/mex" 11 binding="mexNamedPipeBinding" 12 contract="IMetadataExchange"/> 13 </service> 14 </services> 15 <behaviors> 16 <serviceBehaviors> 17 <behavior name="MetadataSupport"> 18 <serviceMetadata/> 19 </behavior> 20 </serviceBehaviors> 21 </behaviors> 22 </system.serviceModel> 23 </configuration>

The first step is about adding a Service Behavior to the behaviors (line 16 - 19). Besides this you also need an endpoint to provide the IMetaDataExchange contract (line 10-12). I chose for an mexNamedPipeBinding because it fits nice beside the netNamedPipeBinding, but you can choose whatever binding type you like, for example: mexTcpBinding and mexHttpBinding.

Now we have set up the Metadata Exchange we can add a Service Reference to generate our proxy. Normally you can use the Discover functionality to Discover Services in your Solution, but sadly this doesn't work for Named Pipes. So instead of automatic discovery I used the url: "net.pipe://localhost/LocalHistory/Service/mex" (make sure you have the service running).

After the discovery of the service you can click the Advanced button to configure some other options. For example you can set the Collection type to be System.Collections.Generic.List instead of System.Array. Also it's the default to reuse types in all referenced assemblies. This is something I wanted to have for the old Webservices for ages, read my other blog entry about this. You don't have newly generated types for all the services types, it just uses the types found in the referenced assembly.

image image

In the Generated code example A you can see that ServiceAction is used from the references assembly instead of a custom generated type.

 

1 public void Do(MM.LocalHistory.Service.Common.ServiceAction action) { 2 base.Channel.Do(action); 3 }

Generated code example A: Use of a referenced assembly.

public void Do(MM.LocalHistory.UI.LocalHistoryWcfServiceInterface.ServiceAction action) { base.Channel.Do(action); }

Generated code example B: Not using a referenced assembly.

A long post around the end of the year. More about the Local History project in the future, and more about WCF I think.

Gravatar