Click here to Skip to main content
Click here to Skip to main content

Using T4 to Generate a Wrapper for a WCF Client

By , 14 Jun 2010
 

Background

The idea for this article came from two places:

  1. I'm writing an application that gets all of its data from a WCF service. Each time I call the WCF service, I end up having to write a bunch of duplicate code and of course I don't like to type, so I was trying to find a way to not have to write that code.
  2. The app I'm developing is an internal business application, and we release weekly. Each release to the WCF services could be incompatible with the previous client. That means that I need to do the release during off hours. I don't like working nights, so I wanted to find a way to have multiple copies of the service running and have the client choose which service to access based on its version, but I didn't want to have to keep changing the config file for the client with each release.

First I'll solve those two problems, and then I'll demonstrate how to use T4 to generate this wrapper from the WCF service reference.

Prerequisites

To run the code, you’ll need to download and install the T4Toolbox.

Problem #1

A typical WCF call looks like this:

var proxy = new WCFServiceWrapper.WcfSvc.Service1Client()
try
{
    returnValue = proxy.GetData(value);
    proxy.Close();
}
catch
{
    proxy.Abort();
    throw;
}
Proxy = null;

To keep from having to write all that code every time I make a call to the WCF service, I created a static class that has a static method with the same signature as the WCF method I'm calling.

public static partial class SvcWrapper
{    
    public static string GetData(int value)
    {
        var proxy = GetServiceClient()
        try
        {
            var returnValue = proxy.GetData(value);
            proxy.Close();
            return returnValue;
        }
        catch
        {
            proxy.Abort();
            throw;
        }
    }
}

Now that same call that took 12 lines of code is now just this one line:

returnValue = SvcWrapper.GetData(value);

Problem #2

To solve the second problem, I just added another method that creates an instance of the service client for me.

public partial class SvcWrapper
{
    private static string _serviceAddress;
    private static string _configName;
    private static bool _needsConfig = true;
    internal static WcfSvc.Service1Client GetServiceClient()
    {
        if (_needsConfig)
        {
            //At this point I'd do some hoopajoop to determine what the 
            //current service address is for this version
            //something like:
 
            //ServiceConfig config = SomeWCFService.GetServiceConfig(versionNo);
            //_serviceAddress = config.Address;
            //_configName = config.ClientEndPointName;
            //The address of the service endpoint
            _serviceAddress = "http://localhost:50324/Service1.svc"
            //This string is the Name of the Client Endpoint 
            //as defined in the running EXE's app.config
            _configName = "WSHttpBinding_IService1";
        }
        return new WCFServiceWrapper.WcfSvc.Service1Client
				(_configName, _serviceAddress);
    }
}

There's nothing earth shattering about that code, and I haven't even implemented the look of the address and config yet, but the shell is there for me to finish at a later date. But now if I want to add another endpoint configuration to the app.config file for this service, I can do that and have only one place to change which endpoint the app uses.

Using T4 to Generate Wrapper Methods

Now, I've solved my original 2 problems, but I've created another one. I'm going to have to create and maintain a wrapper method for every method exposed by the WCF Service. This is the perfect opportunity to do a little code generation with T4.

First thing you need to do is add references to the EnvDTE and T4ToolBox. Then add a new text file called GenerateServiceWrapper.t4. This file holds the code that is not specific to the service we're wrapping, and the t4 extension doesn't create the child .cs file that the .tt extension creates.

This file has 5 methods:

  • GetServiceInterface – takes the name of the service and searches the project for a file that matches. Then it calls FindInterface.
  • FindInterface – takes a project item and searches it for an interface. It returns the first one it finds, and null if it doesn't find one. It could maybe use some error handling but…
  • GetMethodSignature – this takes one of the public methods found on the interface and returns a string that will be the method signature of the wrapper method.
  • GetMethodCall – this takes one of the public methods found on the interface and returns a string that will be the call to that method on the WCF Service.
  • GetServiceWrapper – is where the rubber meets the road. This calls GetServiceInterface to get the interface, loops through the public methods and generates the wrapper methods.

Here’s the contents of that file, you’ll need to get a third party plugin to get syntax highlighting in Visual Studio.

<#@ Template Language="C#" #>
<#@ import namespace="EnvDTE" #>
<#@ include File="T4Toolbox.tt" #>
<#+
public void GetServiceWrapper(string LocalServiceName)
{
    EnvDTE.CodeInterface svcInterface = 
	GetServiceInterface(LocalServiceName + @"\reference.cs");
    foreach (var ce in svcInterface.Members)
    {
        var meth = ce as CodeFunction;
        if (meth != null)
        {
            if (meth.Access == vsCMAccess.vsCMAccessPublic)
            {
                string methodSignature = GetMethodSignature(meth); 
                string methodCall = GetMethodCall(meth);
                bool returnsVoid = false;
                if (meth.Type.AsString.Equals("void"))
                {
                    returnsVoid = true;
                }
                #>
        <#=methodSignature #>
        {
            var proxy = GetServiceClient();
            try
            {
            <#+
                if (returnsVoid)
                {
                       #>    proxy.<#=methodCall #>;
                            proxy.Close();
            <#+
                }
                else
                {
            #>    var returnValue = proxy.<#=methodCall #>;
                proxy.Close();
                return returnValue;
<#+
                }
#>
            }
            catch
            {
                proxy.Abort();
                throw;
            }
        }
        
<#+
            }
        }
    }
}

public EnvDTE.CodeInterface GetServiceInterface(string interfaceFile)
{
    ProjectItem projectItem = TransformationContext.FindProjectItem(interfaceFile);
    FileCodeModel codeModel = projectItem.FileCodeModel;
    return FindInterface(codeModel.CodeElements);
}

public CodeInterface FindInterface(CodeElements elements)
{
    foreach (CodeElement element in elements)
    {
        CodeInterface myInterface = element as CodeInterface;
        if (myInterface != null)
            return myInterface;
        myInterface = FindInterface(element.Children);
        if (myInterface != null)
            return myInterface;
    }
    return null;
}

public string GetMethodSignature(CodeFunction method)
{
    var methodSignature = new System.Text.StringBuilder();
    methodSignature.Append("public static ");
    methodSignature.Append(method.Type.AsString);
    methodSignature.Append(" ");
    methodSignature.Append(method.Name);
    methodSignature.Append("(");
    bool isFirstParameter = true;
    foreach (var prm in method.Parameters)
    {
        CodeParameter p = prm as CodeParameter;
        if (!isFirstParameter)
        {
            methodSignature.Append(", ");
        }
        else
        {
            isFirstParameter = false;
        }
        methodSignature.Append(p.Type.AsString);
        methodSignature.Append(" ");
        methodSignature.Append(p.Name);
    }
    methodSignature.Append(")");
    return methodSignature.ToString();
}

public string GetMethodCall(CodeFunction method)
{
    var methodCall = new System.Text.StringBuilder();
    methodCall.Append(method.Name);
    methodCall.Append("(");
    bool isFirstParameter = true;
    foreach (var prm in method.Parameters)
    {
        CodeParameter p = prm as CodeParameter;
        if (!isFirstParameter)
        {
            methodCall.Append(", ");
        }
        else
        {
            isFirstParameter = false;
        }
        methodCall.Append(p.Name);
    }
    methodCall.Append(")");
    return methodCall.ToString();
}       
#>

Lastly create a text file named ServiceWrapper.tt, or something less generic as this will define the actual wrapper for the WCF Service.

It contains a link to our GenerateServiceWrapper.t4 file, the definition of our class, and a call to GetServiceWrapper to which we pass the name of the WCF Service Reference.

<#@ template language="C#v3.5" hostspecific="True" #>
<#@ include File="T4Templates\GenerateServiceWrapper.t4" #>
using System;

namespace WCFServiceWrapper
{
    public static partial class SvcWrapper
    {    
<#      GetServiceWrapper("WcfSvc");#>
    }
}

Once you save, it should generate a SvcWrapper.cs file with static methods to wrap all of your calls. Now if I want to do something like add logging to each WCF call, all I have to do is add that code to the GetServiceWrapper method in the GenerateService.t4 file, poof all my methods WCF calls are logged.

Notice that I created this class as a partial class, this allows us to put the GetServiceClient method in the same class, but in a separate file. We could have either created that method in a completely separate class, or within the ServiceWrapper.tt file. You never want to edit the generated file, as those edits will be overwritten.

Points of Interest

Syntax highlight for T4 requires a 3rd party plugin. I've tried a few, and they are all pretty weak.

You can't step through T4 code (or I haven't figured out how), so it can take some serious guess and check to figure out what’s going wrong when something goes wrong. The output window is your friend.

History

  • 06/11/10 - First published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Jeremy Hutchinson
Software Developer Winxnet
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5mvpKanasz Robert5 Nov '12 - 2:47 
good article
GeneralMy vote of 5memberPatrick Kalkman19 May '11 - 3:08 
Thanks Jeremy, great article. I should use T4 more often Wink | ;-)
QuestionWhat about data classes?memberTheBC28 Jun '10 - 3:55 
Nice article and code works fine but my services use complex types. How would I go about generating each class after the services class one after each other? Feels like it should be an easy few loops but how can I find the structure to retrieve the data? No intellisense sucks!
 
Cheers!
 
PS, I found this for debugging:
 
http://www.olegsych.com/2008/09/t4-tutorial-troubleshooting-code-generation-errors/[^]
BC Out! Cool | :cool:
 
If at first you don't succeed - DEBUG!

AnswerRe: What about data classes?memberJeremy Hutchinson28 Jun '10 - 4:33 
I did include a sample of how the code generation wrapper works with complex type if it's defined withing the WCF service project. Just search the code for 'CompositeType' (One of the samples auto-generated when I created a WCF service).
 
If you are defining your complex/composite types in a separate project, you just need to add a reference to that project to the wrapper class. After that it should generate successfully.
 
I'm using this code in my project under both scenarios, and haven't had to write anything to loop through the complex types.
 
Or maybe I'm misunderstanding your question.
GeneralRe: What about data classes?memberTheBC28 Jun '10 - 5:32 
Yeah I spotted that but I'm trying to use this as a quick method of generating Java client classes to my .Net WCF services (makes it more fun doesn't it?! Big Grin | :-D ). So I can't reference the project to pick up the definitions. I need to generate each complex type so that it is still available client side.
 
I figured it should just be a matter of looping through the classes in the referenced service project and then looping through the public members of the class. But could do with some pointers on where or where some good docs on the structure are! Any ideas?
BC Out! Cool | :cool:
 
If at first you don't succeed - DEBUG!

GeneralRe: What about data classes?memberTheBC29 Jun '10 - 23:05 
I worked out the solution. The following code can be added to the GenerateServiceWrapper.t4 file (I've followed your format):
 
public void GetDataServiceWrapper(string LocalServiceName)
{
    EnvDTE.CodeInterface svcInterface = GetServiceClasses(LocalServiceName + @"\reference.cs");
    foreach (var projectItem in svcInterface.Children)
    {
		string name = projectItem.GetType().FullName;
		CodeClass codeclass = projectItem as CodeClass;
		if (codeclass != null)
		{
			string projectName = codeclass.Name;
		}
    }
}
 
public EnvDTE.CodeInterface GetServiceClasses(string interfaceFile)
{
    ProjectItem projectItem = TransformationContext.FindProjectItem(interfaceFile);
    FileCodeModel codeModel = projectItem.FileCodeModel;
    return FindClasses(codeModel.CodeElements);
}
 
public CodeInterface FindClasses(CodeElements elements)
{
    foreach (CodeElement element in elements)
    {
		CodeClass cc = element as CodeClass;
		if (cc!= null)
		{
			string classBuilder = GetClass(cc);
			#>
<#=classBuilder #>
	<#+
		}
        CodeInterface myInterface = element as CodeInterface;
        if (myInterface != null)
            return myInterface;
        myInterface = FindClasses(element.Children);
        if (myInterface != null)
            return myInterface;
    }
    return null;
}
 
public string GetClass(CodeClass theClass)
{
    var classBuilder = new System.Text.StringBuilder();
 
    classBuilder.AppendFormat("\tpublic class {0}\r\n", theClass.Name);
	classBuilder.Append("\t{\r\n");
 
    foreach (CodeElement element in theClass.Members)
    {
		if (element.Kind == vsCMElement.vsCMElementProperty)
		{
			CodeProperty property = element as CodeProperty;
			string propertyName = property.Name;
			string type = property.Type.AsString;
            string access = property.Access.ToString();
			string getter = property.Getter.Name;
			string getterType = property.Getter.Type.AsString;
			string setter = property.Setter.Name;
			string setterType = property.Setter.Type.AsString;
			
			if (type != "System.Runtime.Serialization.ExtensionDataObject")
			{
				// Property
				classBuilder.AppendFormat("\t\tprivate {0} m_{1};\r\n", type, propertyName);
				classBuilder.AppendFormat("\t\tpublic {0} {1}\r\n", type, propertyName);
				classBuilder.Append("\t\t{\r\n");
				classBuilder.AppendFormat("\t\t\tget {{ return m_{0}; }}\r\n", propertyName);
				classBuilder.AppendFormat("\t\t\tset {{ m_{0} = value; }}\r\n", propertyName);
				classBuilder.Append("\t\t}\r\n\r\n");
			}
		}
    }
 
    classBuilder.Append("\t}\t\r\n");
    return classBuilder.ToString();
}
 

and then add the following to the SvcWrapper.tt file:
 
<#      GetDataServiceWrapper("WcfSvc");#>
 
that will generate classes in this format:
 
public class CompositeType
	{
		private bool m_BoolValue;
		public bool BoolValue
		{
			get { return m_BoolValue; }
			set { m_BoolValue = value; }
		}
 
		private string m_StringValue;
		public string StringValue
		{
			get { return m_StringValue; }
			set { m_StringValue = value; }
		}
 
	}
 
I'm sure there's a neater solution and I'm only handling what I need to, but this works for me and thought I'd share!
BC Out! Cool | :cool:
 
If at first you don't succeed - DEBUG!

GeneralRe: What about data classes?memberJeremy Hutchinson30 Jun '10 - 1:18 
I don't know jack about interopping with Java, so I'm glad you found what you were looking for.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 14 Jun 2010
Article Copyright 2010 by Jeremy Hutchinson
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid