Adding Installation and Uninstallation Logic to a .Net Component

Last week, we’ve started a series of posts to demonstrate a custom pipeline component used to resolve and select at runtime a flat file schema to use in the disassembling stage.

Before we dwelve into the implementation of the custom component, however, let’s add some features to our Schema Resolver components in order to centralize their installation and make their discovery easier.

In order to centralize the installation of custom Resolver components we will add an installation/uninstallation routine so that each Resolver can register itself under a know registry key.

Installation and uninstallation of .net components is achieved by implementing a class that inherits from the System.Configuration.Install.Installer base class. Together with the InstallUtil.exe command-line utility,

Custom Installation

The following code will prove helpful for custom implementations to provide installation and uninstallation logic to their components:

[RunInstaller(true)]
public class ResolverInstaller : Installer
{
    private const String RegistrySubkey
        = @"SOFTWARE\BizTalk Factory\Pipeline Components\Schema Resolvers";

    #region Operations

    protected Type[] GetResolvers(Assembly assembly)
    {
        List<Type> types = new List<Type>(1);

        foreach (Type type in assembly.GetTypes())
            if (IsDerivedFrom(type, typeof(IResolveSchema)) &&!type.IsInterface && !type.IsAbstract)
                types.Add(type);

        return types.ToArray();
    }

    #endregion

    #region Implementation

    public static bool IsDerivedFrom(Type type, Type baseType)
    {
        if (baseType.IsInterface)
        {
            List<Type> interfaces = new List<Type>(1);
            interfaces.AddRange(type.GetInterfaces());
            return interfaces.Contains(baseType);
        }

        else
            return type.IsSubclassOf(baseType);
    }

    #endregion

First, a GetResolvers method will be used to iterate over all classes that implement the IResolveSchema interface. Note that, for some reason, I could not find an easy way to determine whether a type implements, either directly or indirectly, any given interface. I had to resort to a helper code in the method for this purpose.

    #region Installer Overrides

    public virtual void Register(System.Collections.IDictionary stateSaver)
    {
        RegistryKey hKey = Registry.LocalMachine.OpenSubKey(RegistrySubkey, true);

        // add registry key on first install

        if (hKey == null)
        {
            hKey = Registry.LocalMachine.CreateSubKey(RegistrySubkey);
            PushRegistryKey(stateSaver, RegistrySubkey);
        }

        // install resolvers in the current assembly

        foreach (Type t_Resolver in GetResolvers(Assembly.GetCallingAssembly()))
        {
            Guid guid = Guid.Empty;

            IResolveSchema resolver = SchemaResolverFactory.Activate(t_Resolver);
            resolver.GetClassID(out guid);

            RegistryKey hKeyResolver = hKey.CreateSubKey(guid.ToString("b"));
            PushRegistryKey(stateSaver, guid.ToString("b"));

            hKeyResolver.SetValue("Assembly", t_Resolver.AssemblyQualifiedName);
        }
    }

    public virtual void Unregister(System.Collections.IDictionary savedState)
    {
        RegistryKey hKey = Registry.LocalMachine.OpenSubKey(RegistrySubkey, true);

        foreach (Type t_Resolver in GetResolvers(Assembly.GetCallingAssembly()))
        {
            Guid guid = Guid.Empty;

            IResolveSchema resolver = SchemaResolverFactory.Activate(t_Resolver);
            resolver.GetClassID(out guid);

            hKey.DeleteSubKeyTree(guid.ToString("b"));
        }

        // remove registry key on last uninstall

        if (hKey.SubKeyCount == 0)
            Registry.LocalMachine.DeleteSubKeyTree(RegistrySubkey);
    }

Next, the Register and Unregister methods are used to provide implementers with an easy way to add appropriate registry entries for their custom Resolvers. Note that this code makes use of a PushRegistryKey method that stores each and every registry key created upon installation.

This is useful for providing robust RollBack functionality. In case of installation failure, the InstallUtil command-line utility will call the RollBack override. The code below is designed to restore the registry to its initial state.

    #endregion

    #region Installer Implementation

    public static void PushRegistryKey(System.Collections.IDictionary stateSaver, string subkey)
    {
        // stateSaver cannot contain generic collection types
        // so we use and intermediate array instead

        if (!stateSaver.Contains("RegistryKeys"))
            stateSaver["RegistryKeys"] = new String[]{};

        Stack<String> RegistryKeys = new Stack<string>(stateSaver["RegistryKeys"] as String[]);
        RegistryKeys.Push(subkey);

        stateSaver["RegistryKeys"] = RegistryKeys.ToArray();
    }

    public static string PopRegistryKey(System.Collections.IDictionary savedState)
    {
        if (savedState.Contains("RegistryKeys"))
        {
            // savedState cannot contain generic collection types
            // so we use and intermediate array instead

            Stack<String> RegistryKeys = new Stack<string>(savedState["RegistryKeys"] as String[]);
            if (RegistryKeys.Count > 0)
            {
                string subkey = RegistryKeys.Pop();
                savedState["RegistryKeys"] = RegistryKeys.ToArray();
                return subkey;
            }

            return String.Empty;
        }

        return String.Empty;
    }

    public override void Rollback(System.Collections.IDictionary savedState)
    {
        string subkey = String.Empty;
        while (!String.IsNullOrEmpty(subkey = PopRegistryKey(savedState)))
        {
            int index = subkey.LastIndexOfAny(new char[]{'\\', '/'});
            if (index != -1)
            {
                string parent = subkey.Substring(0, index);
                string name = subkey.Substring(index + 1);

                try
                {
                    RegistryKey hKey = Registry.LocalMachine.OpenSubKey(parent, true);
                    if (hKey != null)
                    hKey.DeleteSubKeyTree(name);
                }
                catch
                {
                    // prevent rollback to crash on uninstall
                }
            }

            else
            {
                try
                {
                    RegistryKey hKey = Registry.LocalMachine.OpenSubKey(RegistrySubkey, true);
                    if (hKey != null)
                    hKey.DeleteSubKeyTree(subkey);
                }
                catch
                {
                    // prevent rollback to crash on uninstall
                }
            }
        }
    }

    #endregion
}

Note that this code rely on custom Resolver components to be registered into the Global Assembly Cache (GAC) for them to be found. You could alter the code slightly in order to save an installation path as well if that is your preference.

Let’s try it out

First, create a new C# class-library project, and add references to Microsoft.BizTalk.Pipeline.

You must also add a reference to the Microsoft.BizTalk.Component.Utilities assembly, but this one is only available in the GAC. So you must manually edit the contents of the C# project file (.csproj) like so:

<Reference Include="Microsoft.BizTalk.Component.Utilities, Version=3.0.1.0, Culture=neutral, PublicKeyToken="31bf3856ad364e35", processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>C:\Windows\assembly\GAC_MSIL\Microsoft.BizTalk.Component.Utilities\3.0.1.0__31bf3856ad364e35\Microsoft.BizTalk.Component.Utilities.dll</HintPath>
</Reference>

Then, let’s add a custom resolver which, for our purposes, will do nothing but resolve any incoming message to a single flat file schema. In real scenarios, you would need to read at least some amount of the incoming message stream in order to recognized its contents.

[Guid("00000000-0000-0000-00000000000000000")]
public class CustomSchemaResolver : SchemaResolverBase
{
    public override DisassemblerProperties Resolve(System.IO.Stream stream)
    {
        // dummy implementation
        return new DisassemblerProperties(
            @"CustomSchema.FlatFileSchema, CustomSchema, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c835a04bdd644ab6");
    }
}

Notice that the class declaration is decorated with a System.Runtime.InteropServices.GuidAttribute attribute. This allows the abstract base class implementation to return a unique identifier for our custom Resolver. You must first change the empty Guid, shown here, with a unique identifier just for you. If you don’t want to decorate the class like this, you will have to provide your own implementation of the GetClassID method.

Finally, we need to provide custom installation and uninstallation logic for our custom Resolvers. Fortunately, most of the grunt work is done in the base class itself, including rollback. The following code will do:

[RunInstaller(true)]
public class CustomResolverInstaller : ResolverInstaller
{
    public override void Install(System.Collections.IDictionary stateSaver)
    {
        Register(stateSaver);
        base.Install(stateSaver);
    }

    public override void Uninstall(System.Collections.IDictionary savedState)
    {
        Unregister(savedState);
        base.Uninstall(savedState);
    }

    public override void Rollback(System.Collections.IDictionary savedState)
    {
        // implementation is required even though the base class does all the work
        base.Rollback(savedState);
    }
}

Now that all Resolver components can be found under a specified registry key, it will be very easy to provide a user-friendly pipeline component configuration dialog box in order to choose from a set of available Resolvers.

That will be the subject of our last post in the series.

This entry was posted in Pipeline Components, Tips. Bookmark the permalink.

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