Sunday, May 24, 2009

WCF - inheriting from a common interface

At work I have a few dozen (and will at some point get to hundreds) workflows with the same structure:

  1. [XmlSerializerFormat]  
  2. [ServiceContract(Namespace = "http://schema.company.com/messages/")]  
  3. public interface IBasicContract<TRequest, TResponse>  
  4.   where TRequest : class  
  5.   where TResponse : class  
  6. {  
  7.   [XmlSerializerFormat]  
  8.   [OperationContract(Name = "GetReport",  
  9.     Action = "http://schema.company.com/messages/GetReport",  
  10.     ReplyAction = "http://schema.company.com/messages/GetReportResponse")]  
  11.   [ServiceKnownType(typeof (Aggregate))]  
  12.   TResponse GetReport(TRequest inquiry);  
  13.   
  14.   [XmlSerializerFormat]  
  15.   [OperationContract(Name = "GetRawReport",  
  16.     Action = "http://schema.company.com/messages/GetRawReport",  
  17.     ReplyAction = "http://schema.company.com/messages/GetRawReportResponse")]  
  18.   string GetRawReport(string guid);  
  19.   
  20.   [XmlSerializerFormat]  
  21.   [OperationContract(Name = "GetArchiveReport",  
  22.     Action = "http://schema.company.com/messages/GetArchiveReport",  
  23.     ReplyAction = "http://schema.company.com/messages/GetArchiveReportResponse")]  
  24.   [ServiceKnownType(typeof (Aggregate))]  
  25.   TResponse GetArchiveReport(string guid);  
  26. }  


I have long wanted to refactor all the workflows to use a common interface, instead of copy-pasting the same code all over the place. Today, after spending 3 days on this, I finally made it :) so I'm saving it here for reference.

This is the common implementation of the above interface:

  1. [ServiceBehavior(Namespace = "http://schema.company.com/messages/")]  
  2. public abstract class BasicWorkflowSvc<TRequest, TResponse, TWorkflow> : IBasicContract<TRequest, TResponse>  
  3.   where TRequest : class  
  4.   where TResponse : class  
  5.   where TWorkflow : class  
  6. {  
  7.   public virtual TResponse GetReport(TRequest inquiry)  
  8.   {  
  9.     TResponse res = null;  
  10.   
  11.     using (var wr = new WorkflowRuntime())  
  12.     {  
  13.       var waitHandle = new AutoResetEvent(false);  
  14.   
  15.       wr.WorkflowCompleted +=  
  16.         delegate(object sender, WorkflowCompletedEventArgs e)  
  17.           {  
  18.             res = (TResponse) e.OutputParameters["wfOutput"];  
  19.             waitHandle.Set();  
  20.           };  
  21.   
  22.       var arguments = new Dictionary<stringobject>();  
  23.       arguments.Add("wfInput", inquiry);  
  24.   
  25.       var wi = wr.CreateWorkflow(typeof (TWorkflow), arguments);  
  26.       wi.Start();  
  27.   
  28.       waitHandle.WaitOne();  
  29.     }  
  30.   
  31.     return res;  
  32.   }  
  33.   
  34.   public virtual string GetRawReport(string guid)  
  35.   {  
  36.     using (var db = new DBAccess())  
  37.       return db.LoadGUID(guid);  
  38.   }  
  39.   
  40.   public abstract TResponse GetArchiveReport(string guid);  
  41. }  


This is the client used to call the workflow:

  1. public class BasicSvcClient<TRequest, TResponse> : ClientBase<IBasicContract<TRequest, TResponse>>, IBasicContract<TRequest, TResponse>  
  2.   where TRequest : class  
  3.   where TResponse : class  
  4. {  
  5.   public BasicSvcClient()  
  6.   {  
  7.   }  
  8.   
  9.   public BasicSvcClient(string endpointConfigurationName) :  
  10.     base(endpointConfigurationName)  
  11.   {  
  12.   }  
  13.   
  14.   public BasicSvcClient(string endpointConfigurationName, string remoteAddress) :  
  15.     base(endpointConfigurationName, remoteAddress)  
  16.   {  
  17.   }  
  18.   
  19.   public BasicSvcClient(string endpointConfigurationName, EndpointAddress remoteAddress) :  
  20.     base(endpointConfigurationName, remoteAddress)  
  21.   {  
  22.   }  
  23.   
  24.   public BasicSvcClient(Binding binding, EndpointAddress remoteAddress) :  
  25.     base(binding, remoteAddress)  
  26.   {  
  27.   }  
  28.   
  29.   public TResponse GetReport(TRequest inquiry)  
  30.   {  
  31.     return Channel.GetReport(inquiry);  
  32.   }  
  33.   
  34.   public string GetRawReport(string guid)  
  35.   {  
  36.     return Channel.GetRawReport(guid);  
  37.   }  
  38.   
  39.   public TResponse GetArchiveReport(string guid)  
  40.   {  
  41.     return Channel.GetArchiveReport(guid);  
  42.   }  
  43. }  


This is how I call the workflow, using the address retrieved from

  1. using (var wf = new BasicSvcClient<ProductRq_Type, ProductRs_Type>(  
  2.   new BasicHttpBinding("httpsDataEndpoint"),  
  3.   new EndpointAddress(tools.FindEndpointAddress("Product.IProductSvc"))))  
  4. {  
  5.   txtConfirmation.Text = wf.GetReport(request).AsXML();  
  6. }  


Finally, these are the helper functions (in the tools static class):

  1. public static ChannelEndpointElement FindEndpoint(string endpointName)  
  2. {  
  3.   var cf = (ClientSection) ConfigurationManager.GetSection("system.serviceModel/client");  
  4.   foreach (ChannelEndpointElement endpoint in cf.Endpoints)  
  5.     if (endpoint.Name == endpointName)  
  6.       return endpoint;  
  7.   
  8.   return null;  
  9. }  
  10.   
  11. public static string FindEndpointAddress(string endpointName)  
  12. {  
  13.   var endpoint = FindEndpoint(endpointName);  
  14.   return endpoint == null ? null : endpoint.Address.ToString();  
  15. }  


Well... hope this helps anyone else who has this idea :)

2 comments:

Cristi Lupașcu said...

Thank you very much, Marcel!

I find this post very helpful, as I ran into a similar situation of duplication across multiple ServiceContract interfaces.

Marcel said...

Thanks for letting me know - I'm glad this helped someone!