Instalace ASP.NET aplikace probíhá obvykle stylem "nakopíruj tohle do rootu webu a modli se". Součástí aplikace ale často bývají různé pomocné utilitky, které při své instalaci a odinstalaci vyžadují různé úkony. Například vložení do GAC, instalaci služby (Windows service), vytvoření protoklu událostí (vlastní event log a nebo alespoň event log source). Microsoft .NET framework na to má infrastrukturu v podobě instalačních tříd: užitečného, leč často přehlíženého nástroje.

Hned na úvod je nutné upozornit na jednu důležitou věc: v tomto článku nebude řeč o vytváření instalačních balíčků, setup projektech, MSI a podobných věcech. Zaměříme se na specifické činnost, které konkrétní assemblies potřebují k životu.Ty lze vyvolat buďto ručně, pomocí utility InstallUtil.exe, která je součástí .NET Frameworku, nebo v rámci "velké" instalace, jako jsou např. různé formy setup projektů ve Visual Studiu.

Jak funguje InstallUtil.exe

Utilitka InstallUtil.exe je součástí .NET Frameworku. Slouží k instalaci a odinstalaci assembly, přičemž záleží pouze na assembly samotné, jaké konkrétní úkony bude instalace a odinstalace zahrnovat. Jedná se o konzolový program, který se spouští z příkazové řádky a ovládá se pomocí parametrů. Jeho nejtypičtější použití nicméně je pouze s názvem assembly, pro instalaci:

InstallUtil.exe C:\Cesta\k\assembly.dll

Odinstalace se pak provádí přidáním parametru /u:

InstallUtil.exe /u C:\Cesta\k\assembly.dll

Assembly přitom může být jak spustitelný program (.exe), tak cokoliv jiného, třeba class library (.dll). Toho jsem využil například ve svém modulu pro obcházení AES chyby v ASP.NET, o kterém jsem psal před pár dny. Je potřeba ho nainstalovat, konkrétně vložit tuto assembly do GAC a zaregistrovat HTTP modul na úrovni serveru.

Vytvářet pro tak jednoduchou aplikaci kompletní setup by byl poněkud overkill. Ruční instalace by byla zase dost komplikovaná, zejména pak proto, že GacUtil.exe, používaná pro ruční instalaci assembly do GAC, není součástí .NET Frameworku ale jen SDK, takže na serveru patrně nebude nainstalovaná. Proto jsem použil instalátory a InstallUtil.exe.

InstallUtil funguje tak, že v dané assembly najde všechy instalační třídy, označené atributem [RunInstaller(true)] a v nich spustí metodu Install, potažmo Uninstall.

Instalační třídy

Pro potřeby instalace se používají instalační třídy (installer classes). Ve své podstatě jde o běžnou třídu, která je poděděná od System.Configuration.Install.Installer. V ní přepíšeme metody Install a Uninstall a do nich doplníme logiku, kterou naše instalace vyžaduje.

Můžeme vytvořit například třídu AssemblyGacInstaller, která vloží aktuální assembly do Global Assembly Cache (a v případě odinstalace ji zase odstraní). Je velmi jednoduchá, nemá žádné parametry, a vypadá takto:

using System;
using System.Collections;
using System.Configuration.Install;

namespace Altairis.Web.OracleBugFix.Install {
    public partial class AssemblyGacInstaller : Installer {

        // Installer methods

        public override void Install(IDictionary stateSaver) {
            // Base install task
            base.Install(stateSaver);

            // Install current assembly to GAC
            try {
                Console.Write("Installing assembly to GAC...");
                var mya = System.Reflection.Assembly.GetExecutingAssembly();
                var p = new System.EnterpriseServices.Internal.Publish();
                p.GacInstall(mya.Location);
                Console.WriteLine("OK");
            }
            catch (Exception ex) {
                Console.WriteLine("Failed!");
                Console.WriteLine(ex.ToString());
            }
        }

        public override void Uninstall(IDictionary savedState) {
            // Base uninstall task
            base.Uninstall(savedState);

            // Remove current assembly from GAC
            try {
                Console.Write("Removing assembly from GAC...");
                var mya = System.Reflection.Assembly.GetExecutingAssembly();
                var p = new System.EnterpriseServices.Internal.Publish();
                p.GacRemove(mya.Location);
                Console.WriteLine("OK");
            }
            catch (Exception ex) {
                Console.WriteLine("Failed!");
                Console.WriteLine(ex.ToString());
            }
        }

    }
}

InstallState

Instalátor může být i podstatně sofistikovanější. Často vyžaduje mezi instalací a odinstalací uchovávání nějakých informací, potřebných pro uvedení počítače do původního stavu. Takové informace si v metodě Install můžete ukládat do kolekce stateSaver. Instalační utilita je potom serializuje do souboru, který se standardně jmenuje JménoAssembly.InstallState. V případě odinstalace se pak informace deserializují a dostanete je v rámci parametru savedState metody Uninstall.

Následující instalační třída slouží k registraci HTTP modulu na úrovni celého web serveru. Do install state si uloží jméno, pod kterým byl  modul do konfigurace vložen:

using System;


using System.Collections;


using Microsoft.Web.Administration;



namespace Altairis.Web.OracleBugFix.Install {


    public partial class HttpModuleInstaller : System.Configuration.Install.Installer {



        // Configuration properties



        public string ModuleName { get; set; }



        public Type ModuleType { get; set; }



        // Installer methods



        public override void Install(IDictionary stateSaver) {


            // Base install tasks


            base.Install(stateSaver);



            // Validate properties


            if (string.IsNullOrEmpty(this.ModuleName)) throw new ArgumentException("ModuleName is null or empty.");


            if (this.ModuleType == null) throw new ArgumentNullException("ModuleType");



            try {


                // Install module to IIS


                Console.Write("Loading server configuration...");


                using (ServerManager serverManager = new ServerManager()) {


                    Configuration config = serverManager.GetApplicationHostConfiguration();


                    ConfigurationSection modulesSection = config.GetSection("system.webServer/modules");


                    ConfigurationElementCollection modulesCollection = modulesSection.GetCollection();


                    Console.WriteLine("OK");



                    Console.Write("Installing module...");


                    ConfigurationElement addElement = modulesCollection.CreateElement("add");


                    addElement["name"] = this.ModuleName;


                    addElement["type"] = this.ModuleType.AssemblyQualifiedName;


                    modulesCollection.Add(addElement);


                    Console.WriteLine("OK");



                    Console.Write("Saving changes...");


                    serverManager.CommitChanges();


                    Console.WriteLine("OK");


                }



                // Add module name to install state


                stateSaver.Add("ModuleName", this.ModuleName);


            }


            catch (Exception ex) {


                Console.WriteLine("Failed!");


                Console.WriteLine(ex.ToString());


            }


        }



        public override void Uninstall(IDictionary savedState) {


            // Base uninstall task


            base.Uninstall(savedState);



            // Load installed module name


            if (savedState == null) throw new ArgumentNullException("savedState");


            var moduleName = savedState["ModuleName"] as string;


            if (string.IsNullOrEmpty(moduleName)) throw new ArgumentException("Module name not available in saved state.");



            try {


                Console.Write("Loading server configuration...");


                using (ServerManager serverManager = new ServerManager()) {


                    Configuration config = serverManager.GetApplicationHostConfiguration();


                    ConfigurationSection modulesSection = config.GetSection("system.webServer/modules");


                    ConfigurationElementCollection modulesCollection = modulesSection.GetCollection();


                    Console.WriteLine("OK");



                    Console.Write("Uninstalling module...");


                    ConfigurationElement element = FindElement(modulesCollection, "add", "name", moduleName);


                    if (element == null) {


                        Console.WriteLine("Not found - no work to do");


                        return;


                    }


                    modulesCollection.Remove(element);


                    Console.WriteLine("OK");



                    Console.Write("Saving changes...");


                    serverManager.CommitChanges();


                    Console.WriteLine("OK");


                }


            }


            catch (Exception ex) {


                Console.WriteLine("Failed!");


                Console.WriteLine(ex.ToString());


            }


        }



        // Helper methods



        private static ConfigurationElement FindElement(ConfigurationElementCollection collection, string elementTagName, params string[] keyValues) {


            foreach (ConfigurationElement element in collection) {


                if (String.Equals(element.ElementTagName, elementTagName, StringComparison.OrdinalIgnoreCase)) {


                    bool matches = true;



                    for (int i = 0; i < keyValues.Length; i += 2) {


                        object o = element.GetAttributeValue(keyValues[i]);


                        string value = null;


                        if (o != null) {


                            value = o.ToString();


                        }



                        if (!String.Equals(value, keyValues[i + 1], StringComparison.OrdinalIgnoreCase)) {


                            matches = false;


                            break;


                        }


                    }


                    if (matches) {


                        return element;


                    }


                }


            }


            return null;


        }



    }


}

Vestavěné instalátory

Výše uvedené instalátory jsou vcelku univerzální a můžete je snadno níže popsaným způsobem použít ve svých aplikacích. Součástí .NET Frameworku jsou již některé hotové instalátory:

Spouštění instalátorů

InstallUtil.exe spustí jenom ty instalátory, jejichž třídy jsou označené atributem [RunInstaller(true)]. Pokud byste ve své aplikaci měli jenom instalátory vlastní výroby, s parametry napsanými přímo v kódu, mohli byste je prostě označit všechny. V praxi to není příliš dobrý nápad, už jenom proto, že činí znovupoužitelnost instalačních tříd dost komplikovanou.

Instalační třídy mohou fungovat hierarchicky. Třída Installer má kolekci Installers, která může obsahovat další instalátory, které se spustí, je-li spuštěn jejich mateřský. V praxi se tedy obvykle postupuje tak, že si v aplikaci vytvoříte jednu třídu, obvykle ji nazývám ApplicationInstaller a v jejím konstruktoru naplníte kolekci Installers všemi dalšími instalátory, které chcete spustit, přičemž jim při této příležitosti také předáte potřebné parametry. Pouze tuto jedinou třídu pak označíte jako [RunInstaller(true)].

Následující kód ukazuje takovou třídu, která aktivuje oba dva výše popsané instalátory a jeden vestavěný, pro vytvoření vlastního event logu:

using System.ComponentModel;

namespace Altairis.Web.OracleBugFix.Install {
    [RunInstaller(true)]
    public class ApplicationInstaller : System.Configuration.Install.Installer {

        public ApplicationInstaller() {
            // Add AssemblyGacInstaller
            this.Installers.Add(new AssemblyGacInstaller());

            // Add HttpModuleInstaller
            this.Installers.Add(new HttpModuleInstaller {
                ModuleName = "AltairisOracleBugFixErrorHandlingModule",
                ModuleType = typeof(ErrorHandlingModule),
            });

            // Add built-in event log installer
            this.Installers.Add(new System.Diagnostics.EventLogInstaller {
                Log = "MujLog",
                Source = "MujSource"
            });
        }

    }
}