Friday, June 21, 2013

Config transformations without msbuild

In my previous post I was writing about automation of build and deployment process. I used SlowCheetah to transform all configuration files (not just web.config) in a solution and some powershell scripting to push the result to target server.

In this post I'm going to tell you how transformation can be done without triggering a build. This might be helpful when you want to send result files for review before deployment.

I would like to thank Outcoldman and AlexBar as their discussion led me to this solution. Consider the following lines:

using Microsoft.Web.XmlTransform;

namespace ConfigTransformer
{
    public class Program
    {
        public static void Main(string[] args)
        {
            string sourceFile = args[0];
            string transformFile = args[1];
            string resultFile = args[2];

            var transformation = new XmlTransformation(transformFile);
            var transformableDocument = new XmlTransformableDocument();
            transformableDocument.Load(sourceFile);
            transformation.Apply(transformableDocument);
            transformableDocument.Save(resultFile);
        }
    }
}

I get input data from command line arguments and use Microsoft.Web.XmlTransform.dll library to perform transformations here. Now it's time to develop this app to a real life solution. Here is the list of arguments we need:
1. Source folder. A path to solution's directory (I want to transform all files in my solution);
2. Destination folder. The app should place transformed files here;
3. Build configuration name. There might be more than one transformation files for each config file (e.g. Release, Debug). It this case it is reasonable to specify which one to use.
4. A list of configuration files to skip. This should be an optional parameter.

Here is new code:

namespace ConfigTransformer
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args == null || args.Length < 3)
            {
                return;
            }

            var transformer = new SolutionConfigsTransformer(args[0], args[1], args[2]);
            if (args.Length > 3)
            {
                for (int i = 3; i < args.Length; i++)
                {
                    transformer.FilesToExclude.Add(args[i]);
                }
            }

            transformer.Transform();
        }
    }
}

In Main I validate input parameters and pass them to SolutionConfigTransformer. This class encapsulates transformation logic and exposes one method Transform(). Here is it's implementation:

public void Transform()
{
    if (!IsInputValid())
    {
        return;
    }

    IList<ConfigurationEntry> configurationEntries = GetConfigurationEntries();
    foreach (ConfigurationEntry entry in configurationEntries)
    {
        var transformation = new XmlTransformation(entry.TransformationFilePath);
        var transformableDocument = new XmlTransformableDocument();
        transformableDocument.Load(entry.FilePath);
        if (transformation.Apply(transformableDocument))
        {
            if (!string.IsNullOrWhiteSpace(entry.FileName))
            {
                var targetDirecory = Path.Combine(TargetDirectory, entry.ParentSubfolder);
                Directory.CreateDirectory(targetDirecory);
                transformableDocument.Save(Path.Combine(targetDirecory, entry.FileName));
            }
        }
    }
}

After some validation I get a list of configuraion files along with corresponding transformations and do almost the same thing as at the beginning (with some extra System.IO function calls). As you may guess GetConfigurationEntries() method plays a key role in program flow.

private IList<ConfigurationEntry> GetConfigurationEntries()
{
    string[] configs = Directory.GetFiles(SourceDirectory, "*.config", SearchOption.AllDirectories);
    var result = new List<ConfigurationEntry>();
    if (configs.Length == 0)
    {
        return result;
    }

    int i = 0;
    while (i < configs.Length - 1)
    {
        string config = configs[i];
        string transformation = configs[i + 1];
        var regex = new Regex(BuildSearchPattern(config.Remove(config.Length - 7, 7)), RegexOptions.IgnoreCase);
        bool found = false;
        while (regex.IsMatch(transformation))
        {
            Match match = regex.Match(transformation);
            if (IsTransformationFound(match) && !found)
            {
                found = true;
                if (FilesToExclude.Contains(config))
                {
                    m_logger.InfoFormat("{0} is in a black list. Won't be processed", config);
                }
                else
                {
                    var entry = new ConfigurationEntry
                    {
                        FilePath = config,
                        FileName = Path.GetFileName(config),
                        ParentSubfolder = GetParentSubfolder(config),
                        TransformationFilePath = transformation
                    };
                    result.Add(entry);
                }
            }

            i++;
            if (i < configs.Length - 1)
            {
                transformation = configs[i + 1];
            }
            else
            {
                break;
            }
        }

        i++;
    }

    return result;
}

I get all *.config files in a source directory. Then I iterate through the list searching for configuration files that have transformations. My main assumption here is that transformations are right after their configuration file in the list; for instance:

connectionStrings.config
connectionStrings.Relese.config
connectionStrings.Debug.config
Web.config
Web.Release.config
Web.Debug.config
Web.Test.config

I understand it's not a 100% accurate way. But it's rather simple and does what I need. I use regular expressions here to check transformations against particular pattern. If they match then use pattern's named group to check against build configuration name. If it matches too then put the configuration with the corresponding transformation to the result list.

It's just a high level description of my approach. If you are interested feel free to download the sources from my github repository.

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete