Getting web services’ WSDL automatically during build
A customer made me this request so I scratched my head to solve the issue. Let’s start with a bit of context.
Why WSDL?
WSDL is the standard for describing a web service, it roots back to end of ‘90s; being a standard, it could be used to exchange contracts between teams. In .NET svcutil.exe can be used to go both ways: generate WSDL from contract interfaces contained in an assembly or the proxy code from the WSDL.
But… there are always exceptions: in .NET 4.0 you may expose a Windows Workflow (WF) as a WCF Service and the contract is dynamically generated. In such cases, when the contract is built on the fly, the WSDL can be obtained only querying the running service.
IIS Express
I opted to use this tool as it is a good replica of IIS but you can avoid any kind of permanent registration and leave your build server clean. You can launch and configure it to host a web service from the command line (see https://www.iis.net/learn/extensions/using-iis-express/running-iis-express-from-the-command-line). It has two modes of operation: the first mode is implies managing an applicationhost.config file, while the second mode requires only the path to the service’s folder. I opted for this latter with these options:
iisexpress /path:path-to-the-service /port:a-fixed-port-number-chose-by-the-build /clr:v4.0 /systray:false
Exporting the WSDL
To export the WDSL, I implemented a couple of classes. The first class IISExpress is based on the ideas found here: using System.Diagnostics.Process class to launch IIS Express and using a Windows message to quit. The second class WsdlManager is the workhorse. It uses System.ServiceModel.Description.MetadataExchangeClient to get the wsdl from the service and it fix-up the URIs embedded in it so the cross-file references use relative URLs instead of absolute URLs. This way, the saved data will be correctly interpreted later.
Wrapping the code in a task
I put the two classes in an MSBuild task named ExportWSDL contained in AutoWSDL.dll. The main reason is to be able to run the code for each interesting project compiled during the build. But how can we identify the projects that contain a service? I looked up for 32f31d43-81cc-4c15-9de6-3fc5453562b6 (Workflow Foundation 4.0) or 3D9AD99F-2412-4246-B90B-4EAA41C64699 (Windows Communication Foundation) in the ProjectTypeGuids element for each project.
There is another issue: the task may receive a project or a solution: in this latter case we need to parse it. Annoyingly there is no documented way, but other people have already investigated for us (here and here).
The body of the task follows.
var si = SolutionInfo.Parse(this.SolutionFileName);
foreach (var pi in si.Projects)
{
if (pi.Types.Contains(ProjectTypes.WorkflowFoundation40)
|| pi.Types.Contains(ProjectTypes.WindowsCommunicationFoundation))
{
Log.LogMessage(MessageImportance.Low, "Launching IIS Express for {0}", pi.ProjectName);
using (var iis = IISExpress.StartApplication(pi.ProjectRoot, this.Port, 4.0))
{
var wm = new WsdlManager(this.OutputFolder);
var services = new List<string>();
services.AddRange(pi.GetFilesOfInterest(".xamlx"));
services.AddRange(pi.GetFilesOfInterest(".svc"));
foreach (var service in services)
{
string wsdlUrl = string.Format(@"https://localhost:{0}/{1}?wsdl", this.Port, service);
var serviceName = System.IO.Path.GetFileNameWithoutExtension(service);
Log.LogMessage(MessageImportance.Low, "Getting WSDL for {0}", serviceName);
wm.MaterializeWsdl(serviceName, new Uri(wsdlUrl));
}//for
}//using
} else {
Log.LogMessage(MessageImportance.Low, "Project {0} is not a Web service", pi.ProjectName);
}//if
}//for
As you see, I adopted the simple heuristic that each Xamlx and Svc file exposes a web service.
This task takes three parameters
Parameter | Description |
SolutionFileName | The full path to the solution file |
Port | The TCP port number where IIS Express listen |
OutputFolder | The folder where the WSDL files are written |
At this point we have an MSBuild task that given a project or solution file, analizes it and get the WSDL for each available service.
Hooking the Visual Studio compile
An elegant way to use this task is to extend the normal Visual Studio build process, in such a way that built the binaries we proceed to run them and get the WSDL metadata.
To trick to tie into the MSBuild architecture is the CustomAfterMicrosoftCommonTargets property: putting the path of an MSBuild file as its value, will trigger the parsing of such after the Microsoft.Common.targets which is at the core of all Visual Studio projects. In our case we use the PrepareForRunDependsOn target: at this point the DLLs are built and ready to execute.
Here is the content of the AutoWSDL.targets file to extend the VS build.
<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="4.0" >
<!-- hook end of compile -->
<PropertyGroup>
<PrepareForRunDependsOn>$(PrepareForRunDependsOn);ExportWSDL</PrepareForRunDependsOn>
</PropertyGroup>
<PropertyGroup>
<WsdlHostingPort Condition="'$(WsdlHostingPort)'==''">44444</WsdlHostingPort>
</PropertyGroup>
<UsingTask TaskName="ExportWSDL" AssemblyFile="AutoWSDL.dll" />
<Target Name="ExportWSDL">
<PropertyGroup>
<WsdlOutputFolder>$(OutDir)WSDL\$(MSBuildProjectName)\</WsdlOutputFolder>
</PropertyGroup>
<MakeDir Condition="!Exists($(WsdlOutputFolder))" Directories="$(WsdlOutputFolder)" />
<ExportWSDL
SolutionFileName="$(MSBuildProjectFullPath)"
OutputFolder="$(WsdlOutputFolder)"
Port="$(WsdlHostingPort)"
/>
</Target>
</Project>
If we put in the same folder the two files, AutoWSDL.targets and AutoWSDL.dll, we can test from a Developer Command Prompt by running:
MSBuild <path_to_a_WCF_project> .csproj /p:CustomAfterMicrosoftCommonTargets= <path_to>AutoWSDL.targets
and find the exported WSDL files under the bin output folder.
Note that you may use this trick to extend in other ways the standard Visual Studio process.
The complete solution
The last logical step is to move from MSBuild to Team Foundation Server build, in order to have a single place for the WSDL contracts shared by the whole team. The nice part is to discover that all we have built before can be reused without changes.
The file AutoWSDLTemplate.xaml is a slightly modified version of the Default Template. It simply adds to the MSBuild Arguments the CustomAfterMicrosoftCommonTargets property.
In XAML code
<Sequence DisplayName="*** Hook CustomAfterMicrosoftCommonTargets">
<Sequence.Variables>
<Variable x:TypeArguments="x:String" Name="customAfterMicrosoftCommonTargets" />
<Variable x:TypeArguments="x:String" Name="localBuildTemplatePath" />
</Sequence.Variables>
<sap:WorkflowViewStateService.ViewState>
<scg:Dictionary x:TypeArguments="x:String, x:Object">
<x:Boolean x:Key="IsExpanded">True</x:Boolean>
</scg:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<mtbwa:ConvertWorkspaceItem Input="[BuildDetail.BuildDefinition.Process.ServerPath]"
Result="[localBuildTemplatePath]" Workspace="[Workspace]" />
<Assign>
<Assign.To>
<OutArgument x:TypeArguments="x:String">[customAfterMicrosoftCommonTargets]</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments="x:String" xml:space="preserve">[Path.Combine(
Path.GetDirectoryName(localBuildTemplatePath),
"AutoWSDL.targets")]</InArgument>
</Assign.Value>
</Assign>
<Assign>
<Assign.To>
<OutArgument x:TypeArguments="x:String">[MSBuildArguments]</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments="x:String">[String.Format(
"{0} /p:CustomAfterMicrosoftCommonTargets={1} ", MSBuildArguments,
customAfterMicrosoftCommonTargets)]</InArgument>
</Assign.Value>
</Assign>
</Sequence>
The first step is to get the Server path of the running build worklow, convert it to a local path using the ConvertWorkspaceItem activity. Then assumes that the AutoWSDL.targets file resides in the same folder; finally adds the CustomAfterMicrosoftCommonTargets property to the list of MSBuild arguments.
This works as long as we have the three files in the same folder.
Final words
I put a complete sample in this ZIP file
I think this is the kind of post that will tickle Mr. Fourie couriosity.
Giulio