Share via


Create an installation package for the WCF-based adapter

Objective: Create a setup project (Setup.exe and/or MSI) for distributing the adapter code and its relevant dependencies using Windows Installer

 

To install the WCF LOB Adapter on the end-user’s machine, at minimum, there is a need to install the assemblies (and any of it pre-requisites) in the Global Assembly Cache (GAC) and then register the WCF custom binding with the WCF configuration. The assemblies can be added to GAC using command gacutil.exe /I and WCF Custom binding can be registered by modifying machine.config. Even though these manual steps may be straightforward for the end-user to follow, in order to make the installation easier, it is recommended to create an installation package in form of MSI or setup.exe that can be used by the end-user to install the product on Windows platform.

 

Once the WCF LOB Adapter is developed, you can create a MSI Installer file to distribute it to the users of the WCF LOB Adapter. Here are the minimum numbers of steps required to create the installation package:

 

n Add a Setup Project in your solution

n Add dependent assemblies in the setup project using Project Output

n Add assemblies to GAC on the target machine

n Add a Custom Action for adding (and removing) the WCF custom binding to (from) WCF configuration in the Setup Project

n Configure the Setup Project as per your requirements

n Build the solution

n Build the Setup Project (by default, it is not automatically built when you built the entire solution)

n Once the build succeeds, right click on the Setup Project and select Install to install the adapter. You can also see a setup.exe and MSI file generated in the Debug/Release folder of the project. Select Install or double click on setup.exe and/or MSI file to install the product.

n If the Setup run successfully without any errors, validate the following

o Ensure you can see the application installed in Control Panel > Add Remove Programs

o Ensure expected entries are added in machine.config under <system.serviceModel> section

o Ensure GAC contains the adapter and dependent assemblies (run Assembly or gacutil /l | find /i “{assembly})

o Ensure the target directory is created containing all the required assemblies

n To uninstall, use Add Remove Programs or right-click on Setup Project and select Uninstall

 

 

This diagram shows the WCF Custom Binding configuration entries required to register the adapter binding with WCF.

 

 

 

The following sections contain a bit more detail on performing the steps outlined above.

 

See the attached ZIP file containing the solution with sample code shown in this post.

 

How to add a Setup Project

Select Add > New Project from the File menu. In the Add New Project dialog box, select the Setup and Deployment category. Select the Setup Project template. Specify a name for the deployment project. This is the name that will be displayed when the end-used runs the installation package. Once the Setup project is created, select the project and update necessary properties such as Title, ProductName, Version, Author and Description.

 

How to include the dependencies into the Setup Project

Select the Setup project, right-click and select Add > Project Output. In Add Project Group Output dialog box, select the DLL or EXE from your solution that should be packaged within the setup project. You can choose other options, but for a simplistic case select Primary output. This will add a Primary Output item within the Setup project. Repeat this for as many dependencies you need to include. If you select the WCF LOB Adapter project, it will automatically also include the adapter’s dependencies such as System.ServiceModel.dll and others.

 

How to add a Custom Action for updating WCF Configuration

Select Add > New Project from the File menu. In the Add New Project dialog box, select the Windows category. Select the Class Library Project template. Fill in the required information and click OK. Right click on Class Library project and Add > New Item > Installer Class. When the file is created, click on Switch to Code. Copy and paste the following code into the installer class. Update the constants defined in the file with the values of your WCF custom binding. Change the namespace for the main and partial class accordingly. Add relevant assemblies so this project can compile. You can also make this a separate component in a different solution and just include the DLL in the setup project. The following code requires System.Configuration and System.ServiceModel assemblies as a reference.

 

using System;

using System.Collections.Generic;

using System.Text;

using System.Reflection;

using System.ServiceModel.Configuration;

using System.Diagnostics;

using System.Configuration;

using System.Configuration.Install;

using System.ComponentModel;

namespace Microsoft.Adapters.Samples

{

    //Custom action to register the adapter with WCF configuration in machine.config

    //<system.serviceModel>

    // <extensions>

    // <bindingElementExtensions>

    // <add name="{BINDINGELEM_NAME}" type="{BINDINGELEM_TYPE}, {Assembly Information}" />

    // </bindingElementExtensions>

    // <bindingExtensions>

    // <add name="{BINDING_NAME}" type="{BINDING_TYPE}, {Assembly Information}" />

    // </bindingExtensions>

    // </extensions>

    // <client>

    // <endpoint binding="{BINDING_NAME}" contract="IMetadataExchange" name="{BINDING_SCHEME}" />

    // </client>

    //</system.serviceModel>

    [RunInstaller(true)]

    public partial class WCFLOBAdapterInstaller : Installer

    {

        private Assembly adapterAssembly;

    private Type bindingSectionType;

        private Type bindingElementExtensionType;

        const string INSTALLER_PARM_INSTALLDIR = "INSTALLDIR";

        const string BINDING_ASSEMBLY_NAME = "MyAdapter.dll";

        const string BINDINGELEM_NAME = "myAdapter";

        // BindingElementExtensionElement derived class

        const string BINDINGELEM_TYPE = "Microsoft.Adapters.Samples.MyAdapterBindingElementExtension";

        const string BINDING_NAME = "myAdapterBinding";

        // StandardBindingCollectionElement<Binding-derived-class, StandardBindingElement-derived-class> derived class

        const string BINDING_TYPE = "Microsoft.Adapters.Samples.MyAdapterBindingCollectionElement";

        const string BINDING_SCHEME = "myscheme";

        /// <summary>

        /// Constructor - initialize the components and register the event handlers

        /// </summary>

        public WCFLOBAdapterInstaller()

        {

            InitializeComponent();

            this.AfterInstall += new InstallEventHandler(AfterInstallEventHandler);

            this.BeforeUninstall += new InstallEventHandler(BeforeUninstallEventHandler);

        }

        /// <summary>

        /// Add the WCF configuration information in machine.config when installing the adapter.

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void AfterInstallEventHandler(object sender, InstallEventArgs e)

        {

            try

            {

                Debug.Assert(this.Context != null, "Context of this installation is null.");

                string path = this.Context.Parameters[INSTALLER_PARM_INSTALLDIR] + BINDING_ASSEMBLY_NAME;

                adapterAssembly = Assembly.LoadFrom(path);

                Debug.Assert(adapterAssembly != null, "Adapter assembly is null.");

                bindingSectionType = adapterAssembly.GetType(BINDING_TYPE, true);

                Debug.Assert(bindingSectionType != null, "Binding type is null.");

                bindingElementExtensionType = adapterAssembly.GetType(BINDINGELEM_TYPE, true);

                Debug.Assert(bindingElementExtensionType != null, "Binding element extension type is null.");

                AddMachineConfigurationInfo();

            }

            catch (Exception ex)

            {

                throw new InstallException("Error while adding adapter configuration information. " + ex.InnerException.Message);

            }

        }

        /// <summary>

        /// Registers the adapter with the WCF configuration

        /// NOTE: The

        /// </summary>

        public void AddMachineConfigurationInfo()

        {

            System.Configuration.Configuration config = ConfigurationManager.OpenMachineConfiguration();

            Debug.Assert(config != null, "Machine.Config returned null");

            // add <client><endpoint>

            ServiceModelSectionGroup sectionGroup = config.GetSectionGroup("system.serviceModel") as ServiceModelSectionGroup;

            if (sectionGroup != null)

            {

                bool channelEndpointElementExists = false;

                // this call can throw an exception if there is problem

                // loading endpoint configurations - e.g. each endpoint

                // tries to load binding which in turn loads the DLL

                ClientSection clientSection = sectionGroup.Client;

                foreach (ChannelEndpointElement elem in clientSection.Endpoints)

                {

                    if (elem.Binding.Equals(BINDING_NAME, StringComparison.OrdinalIgnoreCase) && elem.Name.Equals(BINDING_SCHEME, StringComparison.OrdinalIgnoreCase) && elem.Contract.Equals("IMetadataExchange", StringComparison.OrdinalIgnoreCase))

                    {

                        channelEndpointElementExists = true;

                        break;

                    }

                }

                if (!channelEndpointElementExists)

                {

                    Debug.WriteLine("Adding ChannelEndpointElement for : " + BINDING_NAME);

                    ChannelEndpointElement elem = new ChannelEndpointElement();

                    elem.Binding = BINDING_NAME;

                    elem.Name = BINDING_SCHEME;

                    elem.Contract = "IMetadataExchange";

                    sectionGroup.Client.Endpoints.Add(elem);

                    Debug.WriteLine("Added ChannelEndpointElement for : " + BINDING_NAME);

                }

                // add <bindingElementExtension>

                if (!sectionGroup.Extensions.BindingElementExtensions.ContainsKey(BINDINGELEM_NAME))

                {

                    ExtensionElement ext = new ExtensionElement(BINDINGELEM_NAME, bindingElementExtensionType.FullName + "," + bindingElementExtensionType.Assembly.FullName);

                    sectionGroup.Extensions.BindingElementExtensions.Add(ext);

                }

                // add <bindingExtension>

                if (!sectionGroup.Extensions.BindingExtensions.ContainsKey(BINDING_NAME))

         {

                    ExtensionElement ext = new ExtensionElement(BINDING_NAME, bindingSectionType.FullName + "," + bindingSectionType.Assembly.FullName);

                    sectionGroup.Extensions.BindingExtensions.Add(ext);

                }

                config.Save();

            }

            else throw new InstallException("Machine.Config doesn't contain system.serviceModel node");

        }

        /// <summary>

        /// Remove the machine configuration information when uninstalling the adapter

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void BeforeUninstallEventHandler(object sender, InstallEventArgs e)

        {

            try

            {

                RemoveMachineConfigurationInfo();

            }

            catch (Exception ex)

            {

                throw new InstallException("Error while removing adapter configuration information" + ex.InnerException.Message);

            }

        }

        /// <summary>

        /// Unregisters the adapter with WCF configuration

        /// </summary>

        public void RemoveMachineConfigurationInfo()

        {

            try

            {

                System.Configuration.Configuration config = ConfigurationManager.OpenMachineConfiguration();

                Debug.Assert(config != null, "Machine.Config returned null");

                ServiceModelSectionGroup sectionGroup = config.GetSectionGroup("system.serviceModel") as ServiceModelSectionGroup;

                ChannelEndpointElement elemToRemove = null;

                if (sectionGroup != null)

                {

                    // Remove <client><endpoint>

                    foreach (ChannelEndpointElement elem in sectionGroup.Client.Endpoints)

                    {

                        if (elem.Binding.Equals(BINDING_NAME, StringComparison.OrdinalIgnoreCase) && elem.Name.Equals(BINDING_SCHEME, StringComparison.OrdinalIgnoreCase) && elem.Contract.Equals("IMetadataExchange", StringComparison.OrdinalIgnoreCase))

                        {

                            elemToRemove = elem;

                            break;

                        }

                    }

         if (elemToRemove != null)

                    {

                        Debug.WriteLine("Removing ChannelEndpointElement for : " + BINDING_NAME);

                        sectionGroup.Client.Endpoints.Remove(elemToRemove);

                       Debug.WriteLine("Removed ChannelEndpointElement for : " + BINDING_NAME);

                    }

                    // Remove <bindingExtension> for this adapter

                    if (sectionGroup.Extensions.BindingExtensions.ContainsKey(BINDING_NAME))

  {

                        sectionGroup.Extensions.BindingExtensions.RemoveAt(BINDING_NAME);

                    }

                    // Remove <bindingElementExtension> for this adapter

                    if (sectionGroup.Extensions.BindingElementExtensions.ContainsKey(BINDINGELEM_NAME))

                    {

                        sectionGroup.Extensions.BindingElementExtensions.RemoveAt(BINDINGELEM_NAME);

                    }

                    config.Save();

                }

                else throw new InstallException("Machine.Config doesn't contain system.serviceModel node");

            }

            catch (Exception ex)

            {

                throw new Exception(ex.Message);

            }

        }

        /// <summary>

        /// Use this to test the custom action outside of the Setup Project

        /// </summary>

        /// <param name="path">Path where the adapter DLL can be found</param>

        public static void TestAddConfiguration(Uri path)

        {

            WCFLOBAdapterInstaller action = new WCFLOBAdapterInstaller();

            InstallContext context = new InstallContext();

            // In the Setup project, this is set by selecting custom action

            // and in Properties setting /INSTALLDIR="[TARGETDIR]\" for CustomActionData

            context.Parameters.Add("INSTALLDIR", path.ToString());

            action.Context = context;

            action.AfterInstallEventHandler(null, null);

        }

        /// <summary>

        /// Use this to test the custom action outside of the Setup Project

        /// </summary>

        public static void TestRemoveConfiguration()

        {

            WCFLOBAdapterInstaller action = new WCFLOBAdapterInstaller();

            InstallContext context = new InstallContext();

            action.Context = context;

            action.BeforeUninstallEventHandler(null, null);

        }

    }

}

 

To test this custom action outside of the Setup project, you can create a driver program to test the add/remove configuration functionality. WCF configuration is finicky loading the binding extensions from the WCF configuration, if it cannot load the types from the assemblies – i.e. if the adapter assembly is not in GAC or the name of the configuration elements are specified incorrectly.

 

    class Program

    {

        static void Main(string[] args)

        {

            Uri uri = new Uri(@"C:\_TUTORIALS\AdapterWithSetupProject\MyAdapter\bin\Debug\");

            WCFLOBAdapterInstaller.TestAddConfiguration(uri);

        }

    }

 

If no exceptions are thrown, that means the machine.config was updated successfully. Open machine.config, check for the expected entries and remove them to continue testing with the Setup project.

 

Note: If you run it twice and if the adapter DLL was not in GAC for either this custom binding or any other custom binding, an exception will be thrown by WCF configuration API.

How to include the Custom Action into the Setup Project

Select the Setup project. From main menu, select View > Editor > Custom Actions. Select Custom Actions, right-click and click on Add Custom Action. In Select Item in Project dialog box, select Application Folder and select OK. Click on Add Output. In Add Project Output Group dialog box, select the project containing the custom actions and click on OK. This will include a “Primary output from {class library name} (Active)” entry in Install, Commit, Rollback and Uninstall. Since the Install method in the custom action needs access to the property INSTALLDIR, here is how you can pass the [TARGETDIR] (where the package will be installed) to the Custom Action code. Select “Primary output …” under Install category. In the Properties dialog, set CustomActionData to /INSTALLDIR="[TARGETDIR]\".

 

How to add the assemblies to GAC on target machine

Select the Setup project. From main menu, select View > Editor > File System. Select File System on Target Machine, right-click and select Add Special Folder > Global Assembly Cache Folder. Select Global Assembly Cache Folder, right-click and select Add > Project Output (other options are File and Assembly – use these if these work better for you). In the Add Project Output Group, select the Project you want and click OK.

How to run the installer from the command line

Use this link to learn about various command line options for running msiexec.exe executable.

 

Install: msiexec.exe /i {MSI File Name} /qb

Uninstall: msiexec.exe /x {MSI File Name} /qb

 

** End of post **

AdapterWithSetupProject.zip