Sunday, June 7, 2009

BizTalk 2006 R2 and WCF Fault Messages, Part 2

In my last post, I showed you how to return typed fault exceptions from BizTalk 2006 R2.  Unfortunately, that only solves half of the problem.  The second half of the problem is how to surface those typed faults in the WSDL generated by BizTalk when publishing an orchestration as a web service.  This allows the client side proxy class to properly generate the client side bits that will tie to the typed fault exception.  In this post, I will explain how the BizTalk WCF Publishing Wizard works at a high-level, and how to get the metadata information we need to build the WSDL returned to the clients.  The last post in this series will explain how to get this metadata into the actual WSDL.

Disclaimer:  The explanation below is my understanding of how the BizTalk WCF Publishing Wizard and the BizTalk runtime interact to publish an orchestration as a WCF.  As I am not a member of the BizTalk product team, I am only inferring this information from my investigations.

To add a bit of background, let's look at how BizTalk publishes an orchestration as a service.

First, we run the BizTalk WCF Publishing Wizard on our compiled assembly.  This outputs a few files into the virtual directory that will host our service.  The only one we are concerned with, at this point, is the ServiceDescription.xml.

The ServiceDescription.xml file is the file that gets used by the BizTalk WSDL exporter to generate the actual WSDL that is surfaced to clients.  The problem is that this file does not contain any of the fault information from BizTalk.  This also means that the BizTalk WSDL exporter doesn't have the ability to surface this information even if it existed.

Because of the above issues, our problem is twofold:

  1. We need to get the fault information out of the BizTalk assembly
  2. We then need to put that fault information into the generated WSDL from BizTalk

The first problem actually turns out to be easier to solve than first anticipated.  The BizTalk assemblies contain metadata about the orchestrations, maps, pipelines, etc. and there are BizTalk specific classes that can be used to parse this information (that's how the wizard works).  I found that because of this, the code inside the WCF Publishing Wizard does actually retrieve the fault information, but it is stripped out when the ServiceDescription.xml is written to disk.

My goal was to solve problem #1 in a similar way as the WCF Publishing Wizard.  I wrote a command-line application that parses a BizTalk assembly (using the same classes as the WCF Publishing Wizard) to generate the assembly metadata.  It then parses through that metadata and writes a file called BtsFaultDescription.xml (you can, of course, name it whatever you wish).  This XML file contains the fault information for the ports inside our BizTalk orchestration and will be used to generate the fault information in the WSDL of our service.

The code below shows a modified form of this tool:

(Note:  I can't give out the exact code we use internally, as it contains proprietary information, but I have given as much as needed to solve the problem at hand.)

static void Main(string[] args)
{
    if ((args == null) || (args.Length != 1))
    {
        Console.WriteLine("Usage:  BtsFaultPublisher.exe <assembly_name>");
        return;
    }
 
    string assemblyPath = args[0];
 
    if (!File.Exists(assemblyPath))
    {
        Console.WriteLine("Error:  Assembly '{0}' does not exist.", assemblyPath);
        return;
    }
 
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    BizTalkAssembly bizTalkAssembly = BizTalkAssemblyFactory.Create(assembly);
    bizTalkAssembly.Merge = MergeType.PerAssembly;
    BizTalkAssemblyImporter importer = new BizTalkAssemblyImporter();
    WcfServiceDescription wcfServiceDescription = importer.Import(assembly.Location, bizTalkAssembly);
 
    BtsFaultDescription btsFaultDescription = new BtsFaultDescription();
    List<PortType> portTypes = new List<PortType>();
    foreach (WcfService wcfService in wcfServiceDescription.WcfServices)
    {
        PortType portType = new PortType();
        portType.Name = wcfService.Name;
        List<Operation> operations = new List<Operation>();
        foreach (WcfOperation wcfOperation in wcfService.WcfOperations)
        {
            Operation operation = new Operation();
            operation.Name = wcfOperation.Name;
            List<FaultMessage> faultMessages = new List<FaultMessage>();
            foreach (WcfMessage wcfMessage in wcfOperation.WcfMessages)
            {
                if (wcfMessage.Direction == WcfMessage.WcfMessageDirection.Fault)
                {
                    FaultMessage faultMessage = new FaultMessage();
                    faultMessage.Name = wcfMessage.WcfMessageType.TypeName + "#" + wcfMessage.WcfMessageType.RootName;
                    faultMessage.Description = wcfMessage.WcfMessageType.TypeName + ", " + wcfMessage.WcfMessageType.AssemblyName;
                    faultMessage.FileName = @".\" + wcfMessage.WcfMessageType.TypeName + ".xsd";
                    faultMessage.TypeName = wcfMessage.WcfMessageType.TypeName;
                    faultMessage.AssemblyName = wcfMessage.WcfMessageType.AssemblyName;
                    faultMessage.AssemblyLocation = wcfMessage.WcfMessageType.AssemblyLocation;
                    faultMessage.TargetNamespace = wcfMessage.WcfMessageType.TargetNamespace;
                    faultMessage.RootName = wcfMessage.WcfMessageType.RootName;
                    faultMessages.Add(faultMessage);
                }
            }
 
            if (faultMessages.Count > 0)
            {
                operation.FaultMessage = faultMessages.ToArray();
                operations.Add(operation);
            }
        }
 
        if (operations.Count > 0)
        {
            portType.Operation = operations.ToArray();
            portTypes.Add(portType);
        }
    }
 
    if (portTypes.Count > 0)
    {
        btsFaultDescription.PortType = portTypes.ToArray();
    }
 
    XmlSerializer serializer = new XmlSerializer(typeof(BtsFaultDescription));
    using (XmlTextWriter xmlTextWriter = new XmlTextWriter("BtsFaultDescription.xml", new System.Text.UTF8Encoding(false)))
    {
        xmlTextWriter.Formatting = Formatting.Indented;
        serializer.Serialize((XmlWriter)xmlTextWriter, btsFaultDescription);
        xmlTextWriter.Flush();
    }
 
    Console.WriteLine();
    Console.WriteLine("BtsFaultDescription.xml was written successfully.  Put this file in the App_Data folder of the BTS WCF Service that has been fault-enabled.");
}

The project needs a reference to Microsoft.BizTalk.Adapter.Wcf.Publishing.dll and the following classes.  I put all of them in a file called BtsFaultDescription.cs in a common assembly as these classes are used in this project, as well as the project that writes the actual WSDL.

[Serializable, XmlType(AnonymousType = true), XmlRoot(Namespace = "http://mycompany.com/BizTalk/2009/03/WcfAdapter/Publishing", IsNullable = false), DesignerCategory("code"), GeneratedCode("xsd", "2.0.50727.42")]
public class BtsFaultDescription
{
    private PortType[] portType;
 
    [XmlElement("PortType", Form = XmlSchemaForm.Unqualified)]
    public PortType[] PortType
    {
        get { return this.portType; }
        set { this.portType = value; }
    }
}
 
[Serializable, XmlType(Namespace = "http://mycompany.com/BizTalk/2009/03/WcfAdapter/Publishing"), GeneratedCode("xsd", "2.0.50727.42"), DesignerCategory("code")]
public class Service
{
    private string name;
    private PortType[] portType;
 
    [XmlAttribute]
    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
 
    [XmlElement("PortType", Form = XmlSchemaForm.Unqualified)]
    public PortType[] PortType
    {
        get { return this.portType; }
        set { this.portType = value; }
    }
}
 
[Serializable, XmlType(Namespace = "http://mycompany.com/BizTalk/2009/03/WcfAdapter/Publishing"), GeneratedCode("xsd", "2.0.50727.42"), DesignerCategory("code")]
public class PortType
{
    private string name;
    private Operation[] operation;
 
    [XmlAttribute]
    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
 
    [XmlElement("Operation", Form = XmlSchemaForm.Unqualified)]
    public Operation[] Operation
    {
        get { return this.operation; }
        set { this.operation = value; }
    }
}
 
[Serializable, XmlType(Namespace = "http://mycompany.com/BizTalk/2009/03/WcfAdapter/Publishing"), GeneratedCode("xsd", "2.0.50727.42"), DesignerCategoryK/span>("code")]
public class Operation
{
    private string name;
    private FaultMessage[] faultMessage;
 
    [XmlAttribute]
    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
 
    [XmlElement("FaultMessage", Form = XmlSchemaForm.Unqualified)]
    public FaultMessage[] FaultMessage
    {
        get { return this.faultMessage; }
        set { this.faultMessage = value; }
    }
}
 
[Serializable, XmlType(Namespace = "http://mycompany.com/BizTalk/2009/03/WcfAdapter/Publishing"), GeneratedCode("xsd", "2.0.50727.42"), DesignerCategory("code")]
public class FaultMessage
{
    private string assemblyLocation;
    private string assemblyName;
    private string description;
    private string fileName;
    private string name;
    private string rootName;
    private string targetNamespace;
    private string typeName;
 
    [XmlAttribute]
    public string AssemblyLocation
    {
        get { return this.assemblyLocation; }
        set { this.assemblyLocation = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string AssemblyName
    {
        get { return this.assemblyName; }
        set { this.assemblyName = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string Description
    {
        get { return this.description; }
        set { this.description = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string FileName
    {
        get { return this.fileName; }
        set { this.fileName = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string Name
    {
        get { return this.name; }
        set { this.name = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string RootName
    {
        get { return this.rootName; }
        set { this.rootName = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string TargetNamespace
    {
        get { return this.targetNamespace; }
        set { this.targetNamespace = (string)(value ?? null); }
    }
 
    [XmlAttribute]
    public string TypeName
    {
        get { return this.typeName; }
        set { this.typeName = (string)(value ?? null); }
    }
}

Most of this code was lifted almost verbatim from the publishing wizard, so please forgive some of the code oddities.  Also, be aware that this code was designed to solve one specific scenario, so some of the generation options are hardcoded inside the application.  Eventually, I may extend this to encompass other scenarios, but I thought it was more important to get this information out there.

Another important thing to remember is that the BtsFaultDescription.xml file MUST be regenerated whenever fault information is added/changed/removed in order to keep the generated WSDL in sync with the underlying implementation.  This file is the metadata glue that allows this solution to work.

In the next post, I will show how to actually inject the information in the BtsFaultDescription.xml into the WSDL of the BizTalk WCF Service.