A Smarter WCF Service Client, Part 5

Published on Author Michael

After a long delay I’m finally ready to finish up my series on creating a smarter WCF service client.  Please refer to the previous articles for where we’re at.

  1. Identifying the issues with service references and the existing WCF client code
  2. Creating a smarter WCF service client
  3. Simplifying the calling of a WCF service
  4. Creating the skeleton client template

We’re now ready to generate the T4 template to back the client template.

Creating the Base Template

In order to create the base template we will rely on the lessons learned in my previous articles on T4.  The base template will contain just the configurable options for the template.  Nested templates will be used to generate the different parts of the client code.  We will not discuss how to set up the base T4 template or the actual text template beyond what is needed for the WCF-specific implementation.  Instead refer to the series of articles on T4.

The algorithm for generating the client code will be straightforward.

  1. Retrieve all the WCF service clients from the serviceModel\Client section of the config file. 
  2. For each service client element generate a corresponding service client as defined in previous articles. This includes the extension to the interface for batch calls.  The client name will match the name used in the configuration file.

Within the base template we will allow the following customizations:

  • The project containing the configuration file to examine (the default is the current project).
  • Whether the generated code is public or internal.
  • Change the name of the service client to something other than the name as contained in the config file.
  • Exclude a client entry from generating any code.

To speed up the template generation process the version of the templates that were last published are added to the solution for this series of articles.  They have been updated to support the final version of Visual Studio 2013.  Here’s the base template for generating the service clients.

<#@ template language="C#" hostSpecific="true" #>
<#@ output extension=".txt" #>
<#@ include file="ServiceClientsGenerator.tt" #>
<# 
    //TODO: Make changes here if needed
    var template = new ServiceClientsGenerator() {            
            IsPublic = false,  //Set true to make clients public, generally not recommended
            ConfigurationProject = null, //Optionally set to project containing the endpoints to generate from
    };

    //TODO: Optionally configure endpoint options
    template.ConfigureEndpoint("myendpoint", new EndpointConfiguration() { Name = "YourEndpoint" })
            .ConfigureEndpoint("ignoreEndpoint", new EndpointConfiguration() { Exclude = true });

    template.Run();
#>

Service Clients Generator

The ServiceClientsGenerator.tt file contains the core logic of the template.  It is responsible for loading the configuration file, enumerating the service client endpoints, applying most of the template customizations, validation and starting the generation of the code.

The following code starts the generation process.  It enumerates the endpoints found in the configuration file, gets any customization options and, if not excluded, creates an instance of the ServiceClientTemplate to generate the code.

foreach (ChannelEndpointElement endpoint in ClientInfo.Endpoints)
{   
   //Get the configuration
   var config = GetEndpointConfiguration(endpoint.Name);
	
   if (!config.Exclude)
   {         
      var template = new ServiceClientTemplate() 
      { 
         IsPublic = this.IsPublic, 
         ContractName = config.Name, 
         ContractTypeName = endpoint.Contract, 
         EndpointName = endpoint.Name,
         ExcludeExtensionClass = m_generatedExtensions.Contains(endpoint.Contract)
      };                                            
      InitializeTemplate(template).RenderToFile(config.Name + ".generated.cs");
		
      m_generatedExtensions.Add(template.ContractTypeName);
   };
};

Service Client Template

The ServiceClientTemplate.tt file contains the logic needed to find the service contract and enumerate it.  This brings up an interesting challenge because a contract can be either a part of the solution (perhaps another project) or from a binary reference.  How you retrieve type information differs between the two (reflection vs. the VS code model).  For this post we will use the simpler approach of using reflection.  As a result only contracts added to the project as binary references will work. 

The download has all the code needed to find and load assemblies given the client element in the config file.  You can read the code if you are interested.  Here’s the core template itself.

using System;
using System.ServiceModel;

using Framework.ServiceModel;

namespace <#= DefaultNamespace #>
{    
<#+ Indent(); WriteTypeAttributes(); Unindent(); #>    
   <#= IsPublic ? "public" : "internal" #> partial class <#= ContractName #>Client : ServiceChannelClient<<#= Contract.FriendlyName #>>, <#= Contract.FriendlyName #>
   {
      public <#= ContractName #>Client () : base("<#= EndpointName #>")
      {
      }
<#+     
      Indent(2);

      //Generate the main contract
      var template = new ServiceContractTemplate() { Service = Contract };
      Write(template.TransformText());

      //If there are any inherited ones then generate them as well
      foreach (var service in Contract.GetInheritedContracts())
      {
         WriteLine("");
         var child = new ServiceContractTemplate() { Service = service };
         Write(child.TransformText());
      };
      Unindent(2);
#>
   }

<#+   if (!ExcludeExtensionClass) {
#>
   <#= IsPublic ? "public" : "internal" #> static class <#= Contract.Type.Name #>ServiceClientExtensions
   {
      public static IDisposable OpenPersistentChannel ( this <#= Contract.FriendlyName #> source )
      {
         var batch = source as ISupportsPersistentChannel;
         if (batch != null)
         {
            batch.Open();

            return Disposable.Action(() => batch.Close());
         };

         return Disposable.Empty;
      }
   }
<#+    };
#>	
}

While long, the code is pretty straightforward.  Given a service contract it generates a template based upon the code we developed last time.  The service client name is determined by the config entry or the template configuration with –Client appended to the end.  The client derives from our smart service client defined in previous articles.  Note that the template includes a using statement for the namespace containing the base class.  You will need to change this to match your code.  Additionally the template validates that the assembly is referenced.  You will need to update the validation method as well if you changed the assembly name.

The class is defined as partial so it can be extended.  The default constructor is defined to match the endpoint in the configuration file.  The actual template has an override that allows for changing the name.  Next the contract interface is generated using another template, ServiceContractTemplate.  While not common in WCF contract interfaces may be inherited so any inherited contracts are also generated using new instances of the contract template.

Finally code is generated to add the OpenPersistentChannel method on the interface for batch requests as discussed in previous articles.  The method body matches the skeleton we created last time.  Notice that the code is conditionally defined.  The generator tracks the contracts that are generated and will only generate the extension method for the first contract.  This allows multiple service clients for the same interface.

Service Contract Template

This template is responsible for enumerating the interface and generating the methods to call the WCF service.  This template is simple.

foreach (var operation in Service.GetOperations())
{         
   var template = new InvokeMethodTemplate() { Operation = operation };
   Write(template.TransformText());
};

//Render explicit interface methods
var explicitMethods = (from op in Service.GetOperations() where op.HasCustomName select op);
foreach (var operation in explicitMethods)
{ 
   var template = new ExplicitInterfaceMethodTemplate() { Operation = operation };
   Write(template.TransformText());      
};

For each method defined on the interface (WCF doesn’t use properties) call the InvokeMethodTemplate template to generate the actual method body.  The second loop is for handling custom named methods.  In WCF the name used in an interface can differ from the name used in the contract.  In order to satisfy the interface we have to generate the name as defined in the interface, but the WCF service uses a different name so we have to call the WCF name.  The ExplicitInterfaceMethodTemplate template is responsible for generating the correct logic.  Both of these derive from a base template that provides the core logic.  As such we will only talk about the common case.

Here is the code generated for each method.

public override string TransformText ()
{   
   if (Operation.IsObsolete)
   {   
      if (Operation.ObsoleteIsError) {
#>
[Obsolete("<#= Operation.ObsoleteMessage #>", true)]
<#+      } else {
#>
[Obsolete("<#= Operation.ObsoleteMessage #>")]
<#+
   } }
#>
public virtual <#= Operation.ReturnTypeFriendlyName #> <#= Operation.Name #> ( <#= RenderParameterList() #> )
{
<#+ 
   var outOrRefParameters = (from p in Operation.Parameters where p.IsOutOrRef select p);
   var storeReturnValue = Operation.IsFunction && outOrRefParameters.Any(); 
   //For each out/ref parameter generate a temp variable to pass to the lambda    
   foreach (var parameter in outOrRefParameters) 
   {            
      parameter.GenerateTemporaryName(); #>
<#= parameter.TypeFriendlyName #> <#= parameter.TemporaryName #> = <#= parameter.IsOut ? GetDefaultValueString(parameter) : parameter.Name #>;
<#+    }; #>    
<#= RenderInvokeMethodCall(storeReturnValue) #>
<#+ 
   //For each out/ref parameter generate an assign from temp back to parameter
   foreach (var parameter in outOrRefParameters) 
   {  #>
<#= parameter.Name #> = <#= parameter.TemporaryName #>; 
<#+
   };

   //Return a value if needed
   if (storeReturnValue) 
   { #>    
return results;
<#+     } #>
}
<#+
   return this.GenerationEnvironment.ToString();
} 

Let’s take this one apart.  The first thing it does is look to see if the method on the interface is marked as obsolete (uncommon on public WCF contracts but not private ones).  We’ll come back to that .  Next the method is generated (virtual for extensibility) based upon the method definition on the contract and the call to InvokeMethod is made passing the arguments that were given.  If the method returns a value then it is returned from the method.  Here’s the code for doing that.

private string RenderInvokeMethodCall ( bool storeReturnValue )
{
   var builder = new StringBuilder();

   if (Operation.IsFunction)
   {
      if (storeReturnValue)
         builder.Append("var results = ");
      else
         builder.Append("return ");

      builder.AppendFormat("InvokeMethod<{0}>", Operation.ReturnTypeFriendlyName);
   } else
      builder.Append("InvokeMethod");
                
   builder.AppendFormat("(x => x.{0}({1}));", Operation.MethodName, RenderArguments());
        
   return builder.ToString();
}

Out and Ref Parameters

The challenge here is with out and ref parameters.  Out and ref parameters cannot be passed on to lambda expressions (which is how we invoke the method).  Therefore we have to do the following when there are any out or ref parameters.

  • For each out or ref parameter create a temporary variable with a temporary name
  • For a ref parameter assign the argument to the temporary variable
  • For an out parameter assign the default value for the type to the temporary variable
  • If the method returns a value then create a temporary variable to store the return value
  • Call the method using the adjusted parameter list and return value
  • For each out or ref parameter assign the temporary variable back to the parameter
  • If the method returns a value then return the temporary variable holding the result

Obsolete Methods

As mentioned earlier, obsolete methods are handled specially.  If a method has the Obsolete attribute then it is copied to the client method as well for consistency.  But we don’t want the generated code to cause such messages so we disable the warning with a pragma (for the generated code only).

Note that if the contract marks the method as obsolete so do we but the generated code won’t actually compile if we call it so we just throw a NotSupportedException.  Because the method is marked appropriately no call would actually work.  That code is not shown above but is included in the sample.

Supporting Solution Contracts

As mentioned earlier the solution presented here only works with contracts that are defined using binary references so reflection will work.  If this is too restrictive you can support both source and binary references but there is some work ahead. 

In theory you can support both source (VS code model) and binary (reflection) references but having two different discovery patterns would complicate things.  Instead you should wrap one of the approaches to match the other.  This is an interesting issue because reflection requires a compiled binary (which you may not have) and the VS code model does not allow you to create instances on the fly.  The VS code model is the harder to adapt to for this reason.  Interesting enough the reflection API is based upon abstract types so you can (in theory) create derived types of the core reflection types to expose a contract defined by the VS code model.  For example you could create a derived Type class that exposes a type as defined by the VS code model’s various type models.  This will be significant work but should work if you’re interested.

Ultimately I think the better solution is to simply pick an approach and live with the restriction for the time being.  I believe that when Roslyn is baked into VS in future releases this problem will go away as you will be able to use Roslyn to get all the information whether it is a source or binary reference.  I’m willing to wait myself.

Back to the Sample

The template is now done.  Following the steps given in the T4 articles we can build and install the template into VS.  Now we are ready to go back to our original sample program and update it to use the template.

  • Add a reference to the assembly containing the WCF smart client developed in previous articles.  For this sample it is part of the solution already.
  • Add a new Service Client template to the main application.
  • Ensure the service client element is in the config file
  • Adjust the template configuration as needed
  • Remove any existing service references (and skeleton code in the sample application)

Conclusion

This concludes the series on creating a smarter WCF service client.  With this solution you can eliminate the difficulties of a service reference with an extensible solution that easily fits into your solution with almost no manual code.  The client is exception safe, supports batching and provides a nice abstraction from WCF for testing purposes. 

As an enhancement you could augment the generation code to (optionally) also return async methods based upon the contract.  This could be useful if you want that functionality and wouldn’t be that difficult.  For more local changes the class itself is partial and the methods are virtual so you can make per-project changes to the client as needed.

SmarterWCF.zip

3 Responses to A Smarter WCF Service Client, Part 5

  1. I have posted the core code from the article. It may not quite match the original code that was posted.

  2. Yes, unfortunately my provider is in the process of switching us over to a new blogging platform and they haven’t brought across all the files yet. I’ll contact them to get an ETA for this particular article.

  3. Hi Michael. I’m unable to find a download link in the article to download this code. I’d really like to grab it all and experiment with it.

    Thanks!