Schema validation inside orchestration

BizTalk can do XML schema validation using the XML disassembler pipeline component. But I wanted to the same thing in orchestration to have better control of the error handling and produce better error messages. My solution was to write a .net component that uses the the BtsCatalogExplorer to retrieve the schema from a map name (which I already used to do dynamic mapping).

It is generally not recommended to use BtsCatalogExplorer inside integration solutions – it is intended for administration – but this solution didn’t have to handle high load, and I used caching to avoid populating the object structure over and over again. Here is the code:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.XLANGs.BaseTypes;
using System.Xml.Schema;
using Microsoft.BizTalk.ExplorerOM;
using System.IO;
using System.Xml;
using System.Diagnostics;
using Microsoft.Win32;

namespace SchemaValidatorExample
{
    public class SchemaValidator
    {
        public enum SourceOrTarget { Source, Target };

        private static BtsCatalogExplorer explorer;
        private static Dictionary<string, XmlSchemaSet> schemaCache;

        static SchemaValidator()
        {
            explorer = new BtsCatalogExplorer();
            explorer.ConnectionString = GetMgmtDbConnectionString();
            schemaCache = new Dictionary<string, XmlSchemaSet>();
        }

        private ValidationResult validationResult = new ValidationResult();

        public static ValidationResult Validate(XLANGMessage message, string mapName, SourceOrTarget sourceOrTarget)
        {
            if (string.IsNullOrEmpty(mapName))
                return new ValidationResult();
            Debug.WriteLine("Validating message " + message.Name + " against its schema.", typeof(SchemaValidator).FullName);
            if (message.Count <= 0)
                throw new ApplicationException("Message " + message.Name + " has no parts.");
            // Create an instance of this class to have a ValidationResult instance to hold the validation result.
            // There can be multiple orchestration instances validating documents simultaneously.
            SchemaValidator schemaValidator = new SchemaValidator();
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.ValidationType = ValidationType.Schema;
            // Cannot get schema from message part, because it is untyped.
            settings.Schemas = GetSchemaSet(mapName, sourceOrTarget);
            settings.ValidationEventHandler += new ValidationEventHandler(schemaValidator.DocumentValidationEventHandler);
            XmlReader xmlReader = XmlReader.Create((XmlReader)message[0].RetrieveAs(typeof(XmlReader)), settings);
            //XmlReader xmlReader = XmlReader.Create(message, settings);
            while (xmlReader.Read()) { }
            xmlReader.Close();
            return schemaValidator.validationResult;
        }

        private static XmlSchemaSet GetSchemaSet(string mapName, SourceOrTarget sourceOrTarget)
        {
            XmlSchemaSet xmlSchemaSet = new XmlSchemaSet();
            // Synchronize access to the shared member.
            lock (schemaCache)
            {
                // Schemaset already in cache?
                if (!schemaCache.TryGetValue(mapName, out xmlSchemaSet))
                {
                    // Not in cache – get from BizTalk and add to the cache.
                    List<string> schemaNames = new List<string>();
                    xmlSchemaSet = GetXmlSchemaSetFromBizTalk(GetBtsSchemaFromMapName(mapName, sourceOrTarget), ref schemaNames);
                    schemaCache.Add(mapName, xmlSchemaSet);
                }
            }
            return xmlSchemaSet;
        }

        /// <summary>
        /// Returns an XmlSchemaSet containing the XmlSchema corresponding to the BizTalk schema given as input,
        /// plus all imported schemas (recursively).
        /// </summary>
        /// <param name="btsSchema"></param>
        /// <returns></returns>
        private static XmlSchemaSet GetXmlSchemaSetFromBizTalk(Schema btsSchema, ref List<string> alreadyAdded)
        {
            Debug.WriteLine(string.Format("Reading schema with FullName={0}, TargetNamespace={1}, RootName={2}", btsSchema.FullName, btsSchema.TargetNameSpace, btsSchema.RootName), typeof(SchemaValidator).FullName);
            XmlSchemaSet xmlSchemaSet = new XmlSchemaSet();
            XmlSchema xmlSchema;
            using (TextReader textReader = new StringReader(btsSchema.XmlContent))
            {
                xmlSchema = XmlSchema.Read(textReader, SchemaValidationEventHandler);
            }
            xmlSchemaSet.Add(xmlSchema);
            alreadyAdded.Add(btsSchema.FullName);
            // Add imported schemas
            foreach (XmlSchemaObject include in xmlSchema.Includes)
            {
                if (include is XmlSchemaExternal)
                {
                    XmlSchemaExternal schemaExternal = (XmlSchemaExternal)include;
                    string fullName = schemaExternal.SchemaLocation;
                    if (!alreadyAdded.Contains(fullName))
                        xmlSchemaSet.Add(GetXmlSchemaSetFromBizTalk(explorer.Schemas[fullName], ref alreadyAdded));
                }
            }
            return xmlSchemaSet;
        }

        private static Schema GetBtsSchemaFromMapName(string mapName, SourceOrTarget sourceOrTarget)
        {
            Debug.WriteLine("Getting " + sourceOrTarget + " schema for map " + mapName, typeof(SchemaValidator).FullName);
            int i = mapName.IndexOf(‘,’);
            string transformName = mapName.Substring(0, i).Trim();
            string assemblyName = mapName.Substring(i + 1).Trim();
            Transform transform = explorer.Transforms[transformName, assemblyName];
            if (sourceOrTarget == SourceOrTarget.Source)
                return transform.SourceSchema;
            else
                return transform.TargetSchema;
        }

        /// <summary>
        /// Called when there is a validation error.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        public void DocumentValidationEventHandler(object sender, ValidationEventArgs args)
        {
            validationResult.AddError(args.Message, true);
            Debug.WriteLine(args.Message, GetType().FullName);
        }

        /// <summary>
        /// Called when there is an error loading the XmlSchema itself. That has never happened to me.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        public static void SchemaValidationEventHandler(object sender, ValidationEventArgs args)
        {
            Debug.WriteLine(args.Message, typeof(SchemaValidator).FullName);
            throw new ApplicationException(args.Message);
        }

        private static string GetMgmtDbConnectionString()
        {
            const string regPath = @"SOFTWAREMicrosoftBizTalk Server3.0Administration";
            RegistryKey regKey = Registry.LocalMachine.OpenSubKey(regPath);
            if (regKey == null)
                throw new ApplicationException(string.Format("Registry key {0} not found.", regPath));
            string mgmtDBServer = (string)regKey.GetValue("MgmtDBServer", "(local)");
            string mgmtDBName = (string)regKey.GetValue("MgmtDBName", "BizTalkMgmtDb");
            return string.Format("Server={0};Initial Catalog={1};Integrated Security=SSPI", mgmtDBServer, mgmtDBName);
        }
    }
}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s