Web service management using T4 – Contracts generation (1/2)
In previous post about Model description, we discussed how we could describe webservices using a common XML file that will be used by the tools we will be writing in order to manage them. It is now time to start dealing with all the repetitive tasks and makes your hand dirty. We will start with a basic template to output all contracts in one C# file directly, and we will move to a cleaner solution as tutorials will go.
Previous posts in the series
Introduction & Goals
Model Description
Modeling
As we have seen in the last post, the first step is to describe the Web service model in one common XML file. So, Start Visual Studio, and create a new C# project (of any type for this tutorial) in a new solution. Add a new empty XML file at the root of the project. This file will contain our model description.
In this file, describe your webservices as you like. In my example, I stayed with the declaration I used in the last tutorial, cleaned some tags and added some comments to it.
<?xml version="1.0" encoding="utf-8" ?> <WebServiceModel> <WebService Name="FileService" Comment="Manage all files, including upload and download"> <Operation Name="UploadFile" ReturnType="void" Comment="Upload a file to the web server"> <Argument Name="Filename" Type="string" Comment="Name of the uploaded file"/> <Argument Name="Content" Type="byte[]" Comment="Content of the file"/> <Argument Name="RelativePath" Type="string" Comment="Relative path of the file"/> </Operation> <Operation Name="DownloadFile" ReturnType="byte[]" Comment="Download the file from the server"> <Argument Name="Filename" Type="string" Comment="Downloaded filename"/> <Argument Name="RelativePath" Type="string" Comment="Relative path"/> </Operation> </WebService> <WebService Name="AuthenticationService" Comment="Manage user authentication"> <Operation Name="Connect" ReturnType="Guid" Comment="Connect to the system and return the session id"> <Argument Name="Login" Type="string" Comment="Login name"/> <Argument Name="Password" Type="string" Comment="Password (encoded with salt)"/> </Operation> <Operation Name="Disconnect" ReturnType="void" Comment="Disconnect the given session"> <Argument Name="ConnectionID" Type="Guid" Comment="Session ID"/> </Operation> </WebService> </WebServiceModel>
First script – Parsing the model
Now, we will start by generating one contract for each webservice declared in the XML file. In your project, add a new “Text Template” item called WebServiceContractGenerator.tt at the root of the project.
We will be generating a C# interface in a “.cs” file, so change the “extension” parameter to “.cs”. We will also need to access the “Host”, so change the “hostspecific” parameter to true. I will explain what the Host is later.
<#@ template="" debug="false" hostspecific="true" language="C#" #> <#@ output="" extension=".cs" #>
As we will read XML, we will have to load Linq and XML .NET Assemblies: System.Xml.dll and System.Xml.Linq.dll. We will also have to use Linq and XML existing namespaces of those assemblies. For CLR references, you cannot just add those DLLs to the current project references as we will need those references when we execute the script file using Visual Studio, not when the current project is run. T4 adds specific tags to specify which DLL must be loaded in order to be able to parse the current script files. These tags must be added right at the head of the file.
<#@ assembly name="System.Xml.dll" #> <#@ assembly name="System.Xml.Linq.dll" #> <#@ assembly name="System.Core.dll" #>
For namespaces, you would probably want to add them directly in the script as in a usual C# code file, but it is not possible that way. You have to use other tags to specify which namespaces are imported, at the top of the file.
<#@ import namespace="System.Xml" #> <#@ import namespace="System.Linq"#>
Now, we will have to load the XML file, and parse it.
Start a script block, and create a new XmlDocument
<# //Create a new XML Document XmlDocument doc = new XmlDocument();
Now, we have to Load the XML file. XmlDocument has a Load method that takes the absolute path of the XML file. But how do we know the absolute path? We only know that this file is at the root of our project. We could provide the absolute path directly on our hard drive, but if we move the project, or give it to another developer, it won’t work anymore. This is where the “Host” comes handy. The Host is an object that can be accessed in the script code by using “this.Host”. This object encapsulates the environment that calls the given template. In our example, “Host” encapsulates the Visual Studio Environment. Host exposes a method called “ResolvePath” that takes a relative path and returns the absolute path starting from the current project path. This is exactly what we need. So…
//Retrieve the xml absolute path string xmlPath = this.Host.ResolvePath("WebServiceModel.xml"); //And loads it doc.Load(xmlPath);
Then, we can construct a model that on which we will iterate to output all our C# interfaces. This is a place where anonymous classes come handy.
var xmlModelRoot = doc.SelectSingleNode("descendant::WebServiceModel"); var webServices = from ws in xmlModelRoot.SelectNodes("descendant::WebService").OfType<XmlElement>() select new { Name = ws.GetAttribute("Name"), Comment= ws.GetAttribute("Comment"), Operations = from op in ws.SelectNodes("descendant::Operation").OfType<XmlElement>() select new { Name = op.GetAttribute("Name"), ReturnType = op.GetAttribute("ReturnType"), Comment= op.GetAttribute("Comment"), Arguments= from arg in op.SelectNodes("descendant::Argument").OfType<XmlElement>() select new { Name = arg.GetAttribute("Name"), Type = arg.GetAttribute("Type"), Comment= arg.GetAttribute("Comment") } } };
Now, we can check if our model is loaded successfully. Insert this temporary code (this code will be removed later on).
foreach (var ws in webServices){ WriteLine(ws.Name); foreach (var op in ws.Operations){ WriteLine("t"+op.Name); foreach (var arg in op.Arguments){ WriteLine("tt"+arg.Name); } } } #>
Save the .tt file, and open the WebServiceContractGenerator.cs file. the The ouput should look like this:
So, at this state, the model is well loaded, and can be used to output relevant information. So, let’s output real contracts ! You can remove the last part of the script as we won’t use it (obviously).