Aggregate MyBatis.NET SqlMaps from Multiple C# Projects

Summary

This blog post describes an approach for integrating Spring.NET and MyBatis.NET in a way that lets iBATIS aggregate SqlMap config files from multiple assemblies (a.k.a. assembly scanning) prior to handing out ISqlMapper instances. Teams setting up new MyBatis.NET/Spring.NET tech stacks might find this useful.

The Problem

Your typical enterprise C# ASP.NET web application will have dependencies on multiple C# projects.  Just as best practices with OOAD and Spring support placing our Spring XML configuration files (Spring.NET doesn’t have attribute based configuration … yet) alongside our business components (by namespace or project) rather than keeping all Spring config in a giant single file (thereby limiting reusability of specific business components) we can also follow this process with MyBatis configuration files.  This is not out of the box funcationality with MyBatis.NET. Furthermore, how do you prevent those consuming your C# Project (containing multiple SqlMap config files) from having to know the exact locations of your SqlMap.config files within your assembly?

A typical enterprise Spring.NET/MyBATIS.NET web app

A typical enterprise Spring.NET/MyBATIS.NET web app

Wouldn’t it be nice to have your ASP.NET web application simply discover any new MyBATIS config files when the application starts up?

If you’re on the bleeding edge with iBATIS 3 beta code, then this is a fairly simple task with psuedo code like:

            string[] sqlMaps = Inspector.GetSqlMapsFromAssembly(
                       “MyCompany.Domain1”, “*.SqlMaps.*.xml);
            CodeConfigurationInterpreter codeConfig =
                        new CodeConfigurationInterpreter();
            codeConfig.AddDatabase(new OracleProvider(), “….);
            foreach (string map in sqlMaps)
            codeConfig.AddSqlMap(map, true);

However, for those of us using MyBatis 1.6.x releases in our Spring.NET apps, below is another solution that allows your app to grab all MyBatis.NET config files in a set of dependent C# project.

The Solution

Step 1. – Let Spring know about your dependent projects:

Define a class to hold your config locations like this (yes, Spring.Core.IO.Resource would make much more sense then SearchLocation here…).

    public class SqlMapsHolder : ISqlMapsHolder
    {
        private List embeddedSqlMapSearchLocations;
 
        ///
        /// Two part string: the first representing the NAnt style search pattern, the second is the name of the assembly in which to search.
        ///
        public List EmbeddedSqlMapSearchLocations
        {
            get { return embeddedSqlMapSearchLocations; }
            set { embeddedSqlMapSearchLocations = value; }
        }
 
        /// Take a string of the form "*.Sqlmaps.*.xml, MyCompany.Example" and return it's two parts.
        /// a string like "*.Sqlmaps.*.xml, MyCompany.Example"
       public SearchLocation GetSearchLocation(string searchLocationString)
        {
            char[] delimiter = new char[] { ',' };
            string[] parts = searchLocationString.Split(delimiter, 2);
            return new SearchLocation(parts[0], parts[1]);
        }
    }
 
    /// Holds the distinct NAnt search pattern and assembly name parts.
    public struct SearchLocation
    {
        /// NAnt search pattern and Assembly name
        public string Pattern, Assembly;
 
        /// Constructor
        /// NAnt/Ant search pattern (like *.Sqlmaps.*.xml)
        /// Assembly name (like MyCompany.Example)
        public SearchLocation(string pattern, string assembly)
        {
            Pattern = pattern;
            Assembly = assembly;
        }
    }

Step 2. – Specify your dependent assemblies in your Spring config XML

The configuration below will tell your application to look in the Party, Account and Policy dependent assemblies for any XML files (which are of course marked as “Embedded Resources”) in a folder called SqlMaps (or any other NAnt style syntax you specify) and will pull all of these SqlMaps together before starting up MyBatis. (The SqlMaps you refer to should start with the tag)

This is what you should place in your Spring Config files to tell your app in which assemblies to look for SqlMap config files.

<object id="SqlMapsHolder" type="MyCompany.Util.SqlMapsHolder, MyCompany.Util" >  
    <property name="EmbeddedSqlMapSearchLocations">  
        <list element-type="string">
            <value>*.Sqlmaps.*.xml, MyCompany.Party</value>
            <value>*.Sqlmaps.*.xml, MyCompany.Account</value>
            <value>*.Sqlmaps.*.xml, MyCompany.Policy</value>
        </list>
    </property>
</object>

Where these Sqlmap.xml files look like this:

<?xml version="1.0" encoding="utf-8" ?>
<sqlMap namespace="MyCompany.Party.Sqlmaps" 
xmlns="http://ibatis.apache.org/mapping" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <alias>... </alias>
    <resultMaps>...</resultMaps>
    <statements>...</statements>
</sqlMap>

Step 3. – Create your own SqlMapperFactory.

Let’s call it ConfiguredMapperFactory. Here’s the pseudo-code:

    ///
using System.Configuration;
using IBatisNet.DataMapper;
using Spring.Context;
    /// Used by Spring to initialize a custom MyBATIS.NET singleton Mapper
    ///
    public class ConfiguredMapperFactory : IConfiguredMapperFactory, IApplicationContextAware
    {
        ///
        /// An assembly location in the format:  MyASPNETWebApp.SqlMap.Config.Production.SqlMap.config,MyASPNETWebApp.SqlMap
        ///
        public string ConfigResourceLocation { get; set; }
 
        ///
        /// Container responsible for holding the various SqlMap.xml files that will be merged into one XML document before initializing MyBATIS
        ///
        public ISqlMapsHolder SqlMapsHolder { get; set; }
 
        ///
        /// Singleton instance of our custom MyBatis.NET Mapper
        ///
        public ISqlMapper ConfiguredMapperInstance
        {
            get
            {
                // if the MyBatis.NET config files were not specified via Spring
                if (ConfigResourceLocation == null)
                {
                    // then fall back on getting the config location from web.config
                    lock (typeof(ConfiguredMapper))
                    {
                        string configResourceLocationFromWebConfig =
                            ConfigurationSettings.AppSettings["ibatis.net.config.resource.location"];
                        ConfigResourceLocation = configResourceLocationFromWebConfig;
                        _configuredMapperInstance = ConfiguredMapper.Instance(ConfigResourceLocation, SqlMapsHolder, _applicationContext);
                    }
                }
 
                if (_configuredMapperInstance == null) // braces left out for brevity
                    lock (typeof(ConfiguredMapper))
                        _configuredMapperInstance = ConfiguredMapper.Instance(ConfigResourceLocation, SqlMapsHolder, _applicationContext);
 
                return _configuredMapperInstance;
            }
        }
 
        ///
        /// Getter for the iBATIS.NET ISqlMapper configured based on custom locations of sqlmap.config and providers.config
        ///
        public ISqlMapper GetConfiguredMapperInstance() { return ConfiguredMapperInstance; }
        private ISqlMapper _configuredMapperInstance;

Step 4. – Make a SqlMap Aggregator

We’ll also need a class that knows how to package up all of the iBATIS SqlMaps it finds into a single XML doc that’s fed to MyBATIS:

/// <summary>
/// Merges specified assembly's XML sqlmaps into a SqlMap.config file 
/// </summary>  
public class SqlMapMerger { 
    public static string DATAMAPPER_NAMESPACE_PREFIX = "mapper"; 
    public static string PROVIDERS_NAMESPACE_PREFIX = "provider"; 
    public static string MAPPING_NAMESPACE_PREFIX = "mapping"; 
    public static string DATAMAPPER_XML_NAMESPACE = "http://ibatis.apache.org/dataMapper"; 
    public static string PROVIDER_XML_NAMESPACE = "http://ibatis.apache.org/providers"; 
    public static string MAPPING_XML_NAMESPACE = "http://ibatis.apache.org/mapping"; 
    public static string XML_DATAMAPPER_CONFIG_ROOT = "sqlMapConfig"; 
    public static string XML_SQLMAPS = "sqlMapConfig/sqlMaps";
 
    /// <summary>
    /// Token for xml path to sqlMap elements.
    /// </summary>  
    public static string XML_SQLMAP = "sqlMapConfig/sqlMaps/sqlMap";
 
    ///<summary>
    /// Merge the additional SqlMaps into the Primary SqlMap.config with which we'll initialize iBATIS.NET 
    ///</summary>
    ///<param name="primarySqlMapConfig">The application specific SqlMap.config file</param>
    ///<param name="additionalSqlMapXmlFiles">A container of additional locations in which we'll search for SqlMap XML files</param>
    public XmlDocument Merge(XmlDocument primarySqlMapConfig, ISqlMapsHolder additionalSqlMapXmlFiles) { 
 
        XmlNamespaceManager nsManager = new XmlNamespaceManager(primarySqlMapConfig.NameTable); 
        nsManager.AddNamespace(DATAMAPPER_NAMESPACE_PREFIX, DATAMAPPER_XML_NAMESPACE);
        nsManager.AddNamespace(PROVIDERS_NAMESPACE_PREFIX, PROVIDER_XML_NAMESPACE);
        nsManager.AddNamespace(MAPPING_NAMESPACE_PREFIX, MAPPING_XML_NAMESPACE); 
 
        String mappingPrefix = ApplyDataMapperNamespacePrefix(XML_SQLMAPS); 
        XmlNode sqlMapsNode = primarySqlMapConfig.SelectSingleNode(mappingPrefix, nsManager);
 
        // look at each search location (*.Sqlmaps.*.xml, MyCompany.Project)
        foreach (String searchLocation in additionalSqlMapXmlFiles.EmbeddedSqlMapSearchLocations) { 
            // split it up between the comma 
            SearchLocation location = additionalSqlMapXmlFiles.GetSearchLocation(searchLocation); 
 
            // look for matching files in the assembly 
            IList matchingSqlMaps = AssemblyResourceUtil.GetMatchingResources(location.Assembly, location.Pattern);
 
            // add each of these locations to the primary SqlMap.config's <sqlMaps> section like this: 
            // <sqlMaps>  
            //     <sqlMap embedded="MyCompany.Sqlmaps.Party.xml, MyCompany"/>
            // </sqlMaps>  
            foreach (String fullSqlMapXmlFileName in matchingSqlMaps) { 
                // the "extra" SqlMap that we want to integrate into the main one. 
                XmlNode newSqlMap = primarySqlMapConfig.CreateNode(XmlNodeType.Element, "sqlMap", DATAMAPPER_XML_NAMESPACE); 
                XmlAttribute embeddedLocation = primarySqlMapConfig.CreateAttribute("embedded");
                embeddedLocation.Value = string.Format("{0},{1}", fullSqlMapXmlFileName, location.Assembly);
                newSqlMap.Attributes.Append(embeddedLocation); 
                sqlMapsNode.AppendChild(newSqlMap);
            }
        }
        return primarySqlMapConfig; 
    } 
 
    /// <summary>  
    /// Apply the dataMapper namespace prefix 
    /// </summary>
    public static string ApplyDataMapperNamespacePrefix(string elementName) { 
        return DATAMAPPER_NAMESPACE_PREFIX + ":" + elementName. Replace("/", "/" + DATAMAPPER_NAMESPACE_PREFIX + ":");
    }
}

Step 5. – Create a Class to Configure MyBatis using multiple SqlMap config files

Next, create the ConfiguredMapper that application will use to get instances of the MyBatis ISqlMapper:

using System.Xml;
using IBatisNet.Common.Utilities;
using IBatisNet.DataMapper;
using IBatisNet.DataMapper.Configuration;
using Spring.Context;
 
    /// 
    /// A singleton class to access the SqlMapper defined by the SqlMap.Config
    /// 
    public class ConfiguredMapper
    {
        #region Fields
        private static volatile ISqlMapper _mapper = null;
        #endregion
 
        /// 
        /// static Configure constructor that can be
        /// used for callback
        /// 
        ///
 
        protected static void Configure(object obj)
        {
            _mapper = null;
        }
 
        /// 
        /// Init the 'default' SqlMapper defined by the SqlMap.Config file.
        /// 
        protected static void InitMapper(string configResourceLocation, ISqlMapsHolder additionalSqlMapXmlFiles, IApplicationContext applicationContext)
        {
            DomSqlMapBuilder builder = new DomSqlMapBuilder();
            XmlDocument sqlMapConfig = Resources.GetEmbeddedResourceAsXmlDocument(configResourceLocation); // example "MyCompany.SqlMap.Config.Production.SqlMap.config,MyCompany"
            SqlMapMerger merger = new SqlMapMerger();
            XmlDocument mergedSqlMapConfig = merger.Merge(sqlMapConfig, additionalSqlMapXmlFiles);
            _mapper = builder.Configure(mergedSqlMapConfig);
            // the commented out code below will be the subject of a subsequent blog article:
            // ManagedConnectionStringDataSource managedConnectionStringDataSource = new ManagedConnectionStringDataSource(_mapper.DataSource);
            // managedConnectionStringDataSource.ApplicationContext = applicationContext;
            // inject our own IDataSource so that we can control what is returned when iBATIS calls IDataSource.ConnectionString
            // _mapper.DataSource = managedConnectionStringDataSource;
        }
 
        /// 
        /// Get the instance of the SqlMapper defined by the SqlMap.Config file.
        /// 
        /// A SqlMapper initalized via the SqlMap.Config file.
        public static ISqlMapper Instance(string configResourceLocation, ISqlMapsHolder additionalSqlMapXmlFiles, IApplicationContext applicationContext)
        {
            if (_mapper == null)
            {
                lock (typeof(SqlMapper))
                {
                    if (_mapper == null) // double-check
                    {
                        InitMapper(configResourceLocation, additionalSqlMapXmlFiles, applicationContext);
                    }
                }
            }
            return _mapper;
        }
    }

Step 6. – Wire up your custom Mapper Factory in Spring

<object id="ConfiguredMapperFactory" type="MyCompany.Util.Ibatis.ConfiguredMapperFactory,MyCompany.Util" >
    <property name="ConfigResourceLocation" value="MyASPNETWebApp.Config.SqlMap.config,MyASPNETWebApp"/>
    <property name="SqlMapsHolder" ref="SqlMapsHolder"/>
</object>
 
<object id="IbatisSqlMapper" factory-method="GetConfiguredMapperInstance" factory-object="ConfiguredMapperFactory"/>

Where the SqlMap.config file you refer to is nothing more than:

<?xml version="1.0" encoding="UTF-8" ?> 
<sqlMapConfig xmlns="http://ibatis.apache.org/dataMapper" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">  
    <properties>
        <property key="proxy_userid" value="************"/>
        <property key="proxy_password" value="************"/>
        <property key="userid" value="************"/>
    </properties>
 
    <providers embedded="MyCompany.Util.Config.providers.config,MyCompany.Util"/>
 
    <!-- ==== SqlClient configuration ========= -->  
    <database>  
        <provider name="Oracle Data Provider for .NET"/>  
        <dataSource name="mycompanydatasource" connectionString="Proxy User Id=${proxy_userid};Proxy Password=${proxy_password};User Id=${userid}"/>  
    </database>
 
    <sqlMaps>
        <!-- The SqlMap XML files are now loaded through a Spring-managed Object called SqlMapsHolder -->
    </sqlMaps>
</sqlMapConfig>

Step 7. – Inject the IbatisSqlMapper into your Data Access Objects

<object id="MyPartyDAO" type="MyCompany...." >  
    <property name="SqlMapper" value="IbatisSqlMapper"/>  
</object>

Where SqlMapper is an IBatisNet.DataMapper.ISqlMapper field or property in your Data Access Object.

4 comments

  1. I try to implement this solution, but I don’t find the function AssemblyResourceUtil.GetMatchingResources use in the class SqlMapMerger

  2. Oop. Sorry about that. It’s just a little util class that calls some Spring magic 🙂


    using System;
    using System.Collections;
    using System.IO;
    using System.Reflection;
    using Spring.Util;
    using log4net;

    namespace Foo.Utility
    {
    ///
    /// Used to inspect artifacts of an assembly
    ///
    public class AssemblyResourceUtil
    {

    readonly static ILog _logger = log4net.LogManager.GetLogger(typeof(AssemblyResourceUtil));

    ///
    /// Inspect an assembly, list it's resources, and return matching resources.
    ///
    /// Name of the assembly
    /// The pattern to match against. Use Ant/NAnt like patterns - http://www.springframework.net/doc-latest/reference/html/misc.html
    public static IList GetMatchingResources(string assemblyName, string pattern)
    {
    Assembly assembly = Assembly.Load(assemblyName);
    string[] resources = assembly.GetManifestResourceNames();
    IList matchingResources = new ArrayList();

    Array.Sort(resources);

    foreach (string resource in resources)
    {
    bool result = PathMatcher.Match(
    pattern,
    resource,
    true
    );

    if (result)
    matchingResources.Add(resource);

    _logger.Debug(string.Format("{0}{1}", resource, result ? " [Match!]" : ""));
    }

    return matchingResources;
    }

    ///
    /// Extracts an embedded file out of a given assembly.
    ///
    /// The namespace of you assembly.
    /// The name of the file to extract.
    /// A stream containing the file data.
    public static Stream GetEmbeddedFile(string assemblyName, string fileName)
    {
    try
    {
    Assembly a = Assembly.Load(assemblyName);
    Stream str = a.GetManifestResourceStream(assemblyName + "." + fileName);

    if (str == null)
    throw new Exception("Could not locate embedded resource '" + fileName + "' in assembly '" + assemblyName + "'");
    return str;
    }
    catch (Exception e)
    {
    throw new Exception(assemblyName + ": " + e.Message);
    }
    }
    }
    }

  3. Pingback: episerver

Comments are closed.