Metadata object model extensibility using ExportXmlSchema

Scenario: Adapter Developer needs to define an operation signature using XML Schema types not directly supported by the WCF LOB Adapter SDK Metadata Object Model. For example, there is a need to have an operation signature like this – void SendXmlBlob(System.Xml.XmlElement blob).

WCF LOB Adapter SDK provides metadata object model to define various operation signatures. Consider a scenario where a particular operation should take in “any” XML for an input parameter or output result. I will not go into a philosophical discussion about why we want to use "xsd:any" to define an interface. When defining a service-oriented contract, typically the practice of using xsd:any is not encouraged. The content of this post are still relevant for defining any other custom types not directly supported by the metadata object model. 

In a previous post, I talked about creating a "rich" type metadata (Greeting) using an existing schema.  

In this post, my desired metadata for this outbound operation is as follows:

CLR Interface

[System.ServiceModel.ServiceContractAttribute(Namespace="sample://My.Samples.CodeSnippets", ConfigurationName="SampleContract")]

public interface SampleContract

{

   

    [System.ServiceModel.OperationContractAttribute(Action="SendXmlBlob", ReplyAction="SendXmlBlob/response")]

    [System.ServiceModel.XmlSerializerFormatAttribute()]

    void SendXmlBlob(System.Xml.XmlElement blob);

}

WSDL

<wsdl:definitions xmlns:tns="sample://My.Samples.CodeSnippets" xmlns:wsaw="https://www.w3.org/2006/05/addressing/wsdl" xmlns:ns2="sample://My.Samples.CodeSnippets" targetNamespace="sample://My.Samples.CodeSnippets" xmlns:wsdl="https://schemas.xmlsoap.org/wsdl/">

  <wsdl:types>

    <schema xmlns:ns3="sample://My.Samples.CodeSnippets/PreDefinedTypes" elementFormDefault="qualified" targetNamespace="sample://My.Samples.CodeSnippets" version="1.0" xmlns="https://www.w3.org/2001/XMLSchema">

      <import namespace="sample://My.Samples.CodeSnippets/PreDefinedTypes" />

      <element name="SendXmlBlob">

        <annotation>

          <documentation>

            <doc:action xmlns:doc="https://schemas.microsoft.com/servicemodel/adapters/metadata/documentation">SendXmlBlob</doc:action>

          </documentation>

        </annotation>

        <complexType>

          <sequence>

            <element minOccurs="1" maxOccurs="1" name="blob" type="ns3:XmlBlob" />

          </sequence>

        </complexType>

      </element>

      <element name="SendXmlBlobResponse">

        <annotation>

          <documentation>

            <doc:action xmlns:doc="https://schemas.microsoft.com/servicemodel/adapters/metadata/documentation">SendXmlBlob/response</doc:action>

          </documentation>

        </annotation>

        <complexType>

          <sequence />

        </complexType>

      </element>

    </schema>

    <schema xmlns:b="https://schemas.microsoft.com/BizTalk/2003" xmlns="sample://My.Samples.CodeSnippets/PreDefinedTypes" elementFormDefault="qualified" targetNamespace="sample://My.Samples.CodeSnippets/PreDefinedTypes">

      <element name="blob" xmlns:q1="sample://My.Samples.CodeSnippets/PreDefinedTypes" type="q1:XmlBlob" />

      <complexType name="XmlBlob">

        <sequence>

          <any minOccurs="0" processContents="lax" />

        </sequence>

      </complexType>

    </schema>

  </wsdl:types>

  <wsdl:message name="SampleContract_SendXmlBlob_InputMessage">

    <wsdl:part name="parameters" element="ns2:SendXmlBlob" />

  </wsdl:message>

  <wsdl:message name="SampleContract_SendXmlBlob_OutputMessage">

    <wsdl:part name="parameters" element="ns2:SendXmlBlobResponse" />

  </wsdl:message>

  <wsdl:portType name="SampleContract">

    <wsdl:operation name="SendXmlBlob">

      <wsdl:input wsaw:Action="SendXmlBlob" message="ns2:SampleContract_SendXmlBlob_InputMessage" />

      <wsdl:output wsaw:Action="SendXmlBlob/response" message="ns2:SampleContract_SendXmlBlob_OutputMessage" />

    </wsdl:operation>

  </wsdl:portType>

</wsdl:definitions>

Sample User Code

    class Program

    {

        static void Main(string[] args)

        {

            SampleContractClient proxy = new SampleContractClient();

            XmlDocument document = new XmlDocument();

            // document.LoadXml("<item><name>wrench</name></item>");

            document.Load(@"c:\temp\somexml.xml");

            XmlElement content = document.DocumentElement;

            proxy.SendXmlBlob(content);

        }

    }

This post shows how to get this metadata definition using WCF LOB Adapter SDK.

Step 1: Define a custom XML schema definition

 

XmlBlobType.xsd

<?xml version="1.0" encoding="utf-8"?>

<xsd:schema xmlns:b="https://schemas.microsoft.com/BizTalk/2003" xmlns="sample://My.Samples.CodeSnippets/PreDefinedTypes" elementFormDefault="qualified" targetNamespace="sample://My.Samples.CodeSnippets/PreDefinedTypes" xmlns:xsd="https://www.w3.org/2001/XMLSchema">

  <xsd:element name="blob" type="XmlBlob" />

  <xsd:complexType name="XmlBlob">

    <xsd:sequence>

      <xsd:any minOccurs="0" processContents="lax" />

    </xsd:sequence>

  </xsd:complexType>

</xsd:schema>

Step 2: Create a custom type metadata and use the provided XML schema type

 

                XmlBlobTypeMetadata.cs

    class XmlBlobTypeMetadata : TypeMetadata

    {

        private const string METADATA_PATH = @"c:\temp\";

        public XmlBlobTypeMetadata(string typeId, string typeName) : base(typeId, typeName)

        {

        }

        public override void ExportXmlSchema(XmlSchemaExportContext schemaExportContext, MetadataLookup metadataLookup, TimeSpan timeout)

        {

            if (schemaExportContext == null)

            {

                throw new AdapterException("schemaExportContext is null");

            }

            // Find a way to either read in a schema file or create XmlSchema

            // object yourself

            // for this example, use the type name to load a schema

            XmlReader reader = XmlReader.Create(METADATA_PATH + "XmlBlobType.xsd");

            XmlSchema schema = XmlSchema.Read(reader, null);

            if (!IsComplexTypeAlreadyDefined(schemaExportContext.SchemaSet, schema))

            {

                schemaExportContext.SchemaSet.Add(schema);

                schemaExportContext.NamespacePrefixSet.Add("othertypes", "sample://My.Samples.CodeSnippets/PreDefinedTypes");

            }

            reader.Close();

        }

        public static bool IsComplexTypeAlreadyDefined(XmlSchemaSet oldschemaset, XmlSchema newschema)

        {

            // ensure correct namespace was defined in the passed-in schema

            foreach (XmlSchema schema in oldschemaset.Schemas(newschema.TargetNamespace))

   {

                foreach (XmlSchemaObject newschemaObject in newschema.Items)

                {

                    if (newschemaObject is XmlSchemaComplexType)

                    {

                        //check for the definition of complex type in the schemaset

                        foreach (XmlSchemaObject schemaObject in schema.Items)

                        {

                            XmlSchemaComplexType complexType = schemaObject as XmlSchemaComplexType;

                  // Definition of this Complex Type already exists

                            if (complexType != null && String.Compare(complexType.Name, ((XmlSchemaComplexType)newschemaObject).Name, false, System.Globalization.CultureInfo.InvariantCulture) == 0)

                                return true;

                        }

                    }

                }

            }

            return false;

        }

        public override bool CanSetDefaultValue

        {

            get { return false; }

        }

        public override System.Xml.XmlReader CreateXmlReader(AdapterDataReader dataReader)

        {

            return null;

        }

        public override System.Xml.XmlDictionaryWriter CreateXmlWriter(AdapterDataWriter dataWriter)

        {

            return null;

        }

    }

Step 3: Use the custom type metadata within the metadata resolver

This step is required for the adapter to generate the XML Schema metadata for an operation.

                AdapterMetadataResolverHandler.cs

        public OperationMetadata ResolveOperationMetadata(string operationId, TimeSpan timeout, out TypeMetadataCollection extraTypeMetadataResolved)

        {

            extraTypeMetadataResolved = null;

            // for illustrative purposes show how to resolve one operation here

            ParameterizedOperationMetadata opMetadata = new ParameterizedOperationMetadata(operationId, "");

            // Setting opMetadata.DisplayName is mandatory, otherwise metadata won't be generated

            opMetadata.OperationNamespace = SampleAdapter.SERVICENAMESPACE;

            opMetadata.OperationGroup = "SampleContract";

            switch (operationId)

            {

                case "SendXmlBlob":

                    // Syntax: void SendXmlBlob(XmlElement blob)

                    opMetadata.DisplayName = "SendXmlBlob";

                    // Create Operation Parameters

                    ComplexQualifiedType xmlBlobQT = new ComplexQualifiedType("XmlBlobType");

                    OperationParameter blobParm = new OperationParameter("blob", OperationParameterDirection.In, xmlBlobQT, false);

                    opMetadata.Parameters.Add(blobParm);

                    // Create Operation Result

                    opMetadata.OperationResult = OperationResult.Empty;

                    // Resolve extra type metadata here, instead of using ResolveTypeMetadata method

                    extraTypeMetadataResolved = new TypeMetadataCollection();

                    // Use a pre-defined schema to generate metadata for this type

                    TypeMetadata xmlBlobTM = new XmlBlobTypeMetadata("XmlBlobType", "XmlBlob");

                    // Ensure this namespace matches the namespace the schema belongs to

                    xmlBlobTM.TypeNamespace = SampleAdapter.SERVICENAMESPACE + "/PreDefinedTypes";

                    // Use common cache

                    xmlBlobTM.CanUseCommonCache = true;

                    // Add to the extra type metadata collection

                    extraTypeMetadataResolved.Add(xmlBlobTM);

                    break;

                default:

                    throw new AdapterException("Invalid operation " + operationId);

            }

            return opMetadata;

        }

At this point, if you like, you can generate the WSDL using your adapter to verify that correct WSDL is being generated.

Step 4: Define the operation within the browse handler

Implement this step so that the Adapter Consumer can use the Add Adapter Service Reference Visual Studio Plug-In/Consume Adapter Service BizTalk Project Add-In to browse for operations and then generate the metadata for selected operations.

                AdapterMetadataBrowseHandler.cs

        public MetadataRetrievalNode[] Browse(string nodeId

            , int childStartIndex

            , int maxChildNodes, TimeSpan timeout)

        {

            List<MetadataRetrievalNode> nodes = new List<MetadataRetrievalNode>(5);

            // Root Node

            if (MetadataRetrievalNode.Root.NodeId.Equals(nodeId))

            {

                // Create Category Node

                MetadataRetrievalNode calculatorCategory = new MetadataRetrievalNode("/MyCategory");

                calculatorCategory.Description = "Contains operations that …";

                calculatorCategory.Direction = MetadataRetrievalNodeDirections.Outbound;

                calculatorCategory.DisplayName = "My Category";

                calculatorCategory.IsOperation = false;

                // Add categories to the root node

                nodes.Add(calculatorCategory);

            }

            else

            {

                switch (nodeId)

                {

             case "/MyCategory":

                        // Create SendXmlBlob Operation Node

                        MetadataRetrievalNode sendXmlBlob = new MetadataRetrievalNode("SendXmlBlob");

                        sendXmlBlob.Description = "This operation sends in any XML to the target.";

                        sendXmlBlob.Direction = MetadataRetrievalNodeDirections.Outbound;

                        sendXmlBlob.DisplayName = "SendXmlBlob";

                        sendXmlBlob.IsOperation = true;

                        // Add operations to this category

                        nodes.Add(sendXmlBlob);

                        break;

                }

            }

            return nodes.ToArray();

        }

 

Step 5: Implement the outbound handler to work with the XML message

The implementation of this method is unique to the functionality of each adapter. In the execute method, do whatever you want to do with the incoming WCF message.

The execute() function expects the following input WCF Message for the operation action “SendXmlBlob” based on the metadata defined above.

NOTE: if you want to ignore the metadata for an operation action and just take the input XML for that operation, read this post (go to the bottom to see execute function implemenation). For example, instead of <SendXmlBlob> it could be any operation as retrieved from a WSDL/contract used by the adapter consumer. Instead of <blob>, it will be the operation parameters serialized as XML. The adapter is basically used as a transport binding in that scenario.

<envelope>

                <header> </header>

                <body>

                                <SendXmlBlob>

                                                <blob>

                                                {any XML document}

                                                </blob>

                                </SendXmlBlob>

                </body>

</envelope>

                AdapterOutboundHandler.cs

        public Message Execute(Message message, TimeSpan timeout)

        {

            // Typically, here the Adapter Developer will map the

            // incoming WCF message to the target system message.

            // However, for illustrative purposes, we will just return

            // some dummy responses back

            // Get the OperationMetadata definition for this incoming operation

            OperationMetadata om = this.MetadataLookup.GetOperationDefinitionFromInputMessageAction(message.Headers.Action, timeout);

            if (om == null)

            {

                // This means there is no metadata information defined in the

                // adapter. We can use this if the adapter doesn't really care

                // about the incoming message and just wants to work with the

                // SOAP message without caring about the payload.

                SampleAdapterUtilities.Trace.Trace(TraceEventType.Error, "SampleAdapterOutboundHandler::excute", "Unknown operation " + message.Headers.Action);

                throw new AdapterException("This adapter doesn't understand the operation " + message.Headers.Action);

            }

            // The adapter understands the metadata for the incoming

            // XML message. The processing of this method may require

            // referring to this metadata - e.g. figuring out what

            // back-end function to call and mapping individual XML

      // elements to the target system format.

            switch (message.Headers.Action)

            {

                case "SendXmlBlob":

                    return SendXmlBlob(om, message, timeout);

            }

            return null;

        }

        #endregion IOutboundHandler Members

        private Message SendXmlBlob(OperationMetadata om, Message message, TimeSpan timeout)

        {

            // Expected XML message: <SendXmlBlob><blob>{XML BLOB}</blob></SendXmlBlob>

            XmlWriterSettings settings = new XmlWriterSettings();

            settings.OmitXmlDeclaration = true;

      

            // Read request and do whatever you want to do with the XML blob

            using (XmlDictionaryReader xdr = message.GetReaderAtBodyContents())

            {

                bool foundBlob = xdr.ReadToDescendant("blob");

                if (foundBlob)

                {

                    XmlWriter writer = XmlWriter.Create(@"c:\temp\outputblob.xml", settings);

                    writer.WriteRaw(xdr.ReadInnerXml());

                    writer.Close();

                }

            }

     // Build response

            StringBuilder outputString = new StringBuilder(100);

            XmlWriter replywriter = XmlWriter.Create(outputString, settings);

            replywriter.WriteStartElement(om.DisplayName + "Response", SampleAdapter.SERVICENAMESPACE);

            replywriter.WriteStartElement(om.DisplayName + "Result", SampleAdapter.SERVICENAMESPACE);

            replywriter.WriteEndElement();

            replywriter.WriteEndElement();

            replywriter.Close();

            XmlReader replyReader = XmlReader.Create(new StringReader(outputString.ToString()));

            return Message.CreateMessage(MessageVersion.Default, om.OutputMessageAction, replyReader);

        }

Once you have these implementations provided, build, install and test the adapter.