Skip to content

Setup for ASP.NET Core application

yurislav edited this page Oct 15, 2020 · 6 revisions

This is an example of creating installer for ASP.NET Core application with:

  • windows service
  • firewall exception
  • software dependencies installation
    • .NET Core and Hosting
    • Windows hotfixes (KB)
  • Multilanguage support: English and Slovak (sk-SK) in this example

Create new .NET Framework console applicaton and install following nuget packages:

The code example below expects Assets folder in project directory with following structure:

├───Assets
│   │   background.bmp
│   │   banner.bmp
│   │   icon.ico
│   │   license.rtf
│   │
│   ├───Bootstrapper
│   │   │   bootstrapper-logo-64x64.png
│   │   │
│   │   ├───Dependencies
│   │   │       NDP452-KB2901907-x86-x64-AllOS-ENU
│   │   │       dotnet-hosting-3.1.3-win.exe
│   │   │       Windows6.1-KB2533623-x64.msu
│   │   │       Windows6.1-KB2533623-x86.msu
│   │   │       Windows6.1-KB2999226-x64.msu
│   │   │       Windows6.1-KB2999226-x86.msu
│   │   │       Windows8.1-KB3118401-x64.msu
│   │   │       Windows8.1-KB3118401-x86.msu
│   │   │
│   │   └───Localizations
│   │           thm.sk-SK.wxl
│   │
│   ├───Files
│   │   └───ProgramMenu
│   │           Online docs.url
│   │
│   └───Localizations
│           WixUI_sk-SK.wxl

Online dependencies must be stored locally on your machine during compiling the installer. Dependencies used in this example can be downloaded here:

File name Original source
dotnet-hosting-3.1.3-win.exe https://download.visualstudio.microsoft.com/download/pr/ff658e5a-c017-4a63-9ffe-e53865963848/15875eef1f0b8e25974846e4a4518135/dotnet-hosting-3.1.3-win.exe
NDP452-KB2901907-x86-x64-AllOS-ENU.exe https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe
Windows6.1-KB2533623-x64.msu https://download.microsoft.com/download/F/1/0/F106E158-89A1-41E3-A9B5-32FEB2A99A0B/Windows6.1-KB2533623-x64.msu
Windows6.1-KB2533623-x86.msu https://download.microsoft.com/download/2/D/7/2D78D0DD-2802-41F5-88D6-DC1D559F206D/Windows6.1-KB2533623-x86.msu
Windows6.1-KB2999226-x64.msu https://download.microsoft.com/download/1/1/5/11565A9A-EA09-4F0A-A57E-520D5D138140/Windows6.1-KB2999226-x64.msu
Windows6.1-KB2999226-x86.msu https://download.microsoft.com/download/4/F/E/4FE73868-5EDD-4B47-8B33-CE1BB7B2B16A/Windows6.1-KB2999226-x86.msu
Windows8.1-KB3118401-x64.msu https://download.microsoft.com/download/F/E/7/FE776F83-5C58-47F2-A8CF-9065FE6E2775/Windows8.1-KB3118401-x64.msu
Windows8.1-KB3118401-x86.msu https://download.microsoft.com/download/5/E/8/5E888014-D156-44C8-A25B-CA30F0CCDA9F/Windows8.1-KB3118401-x86.msu

Paste and customize following content to the Program.cs file:

using NineDigit.WixSharpExtensions;
using NineDigit.WixSharpExtensions.Expressions;
using NineDigit.WixSharpExtensions.Localization;
using NineDigit.WixSharpExtensions.Resources;
using System;
using System.IO;
using System.Windows;
using WixSharp;
using WixSharp.Bootstrapper;

namespace WixsharpExtensionsExample
{
    public class Program
    {
        // Company
        const string CompanyFullName = "My company, inc.";
        const string CompanyFolderName = "MyCompany";

        // Product
        const string ProductFullName = "My App";
        const string ProductDescription = "My Application";
        const string ProductComments = "My application comment";
        const string ProductFolderName = "MyApp";
        const string ProductProgramMenuFolderName = "My Application"; // name of folder in start menu
        const string ProductHelpUrl = @"https://www.myapp.example/help";
        const string ProductAboutUrl = @"https://www.myapp.example/about";
        const string ProductContact = @"[email protected]";
        readonly static Guid ProductUpgradeCode = new Guid("PUT-YOUR-PRODUCT-GUID-HERE"); // same value for all versions; must not be changed
        readonly static string ProductLicenceRTFFilePath = Path.Combine("Assets", "license.rtf");
        readonly static string ProductIconFilePath = Path.Combine("Assets", "icon.ico");

        // Product Service
        const string ServiceName = "My App service name";
        const string ServiceDescription = "My App service description";
        const string ServiceDisplayName = "My App service display name";
        const string ServiceExecutableFileName = "MyApp.exe";

        // Product Installer
        readonly static string InstallerBackgroundImagePath = Path.Combine("Assets", "background.bmp");
        readonly static string InstallerBannerImagePath = Path.Combine("Assets", "banner.bmp");
        const string InstallerOutputFileNamePrefix = "my-app-v"; // name of .msi installer file

        // Signing certificate
        const string CertificateThumbprint = "INSERT-YOUR-CERTIFICATE-THUMPRINT";
        const string CertificateTimestampUrl = @"http://timestamp-server.example";

        // Bootstrapper
        readonly static Guid BootstrapperUpgradeCode = new Guid("PUT-YOUR-BOOTSTRAPPER-GUID-HERE");
        readonly static string BootstrapperAssetsDirectoryPath = Path.Combine("Assets", "Bootstrapper");
        readonly static string BootstrapperLogoFilePath = Path.Combine(BootstrapperAssetsDirectoryPath, "bootstrapper-logo-64x64.png");
        readonly static string BootstrapperDependenciesDirectoryPath = Path.Combine(BootstrapperAssetsDirectoryPath, "Dependencies"); // Path to bootstrapper dependencies on your computer
        const string BootstrapperDependenciesDownloadUrlParentPath = @"https://www.myapp.example/dependencies"; // This is where bootstrapper dependencies will be stored for online installer

        static void Main(string[] _)
        {
            var version = new Version(1, 0, 0); // you may want to get version from assembly dynamically with Tasks.GetVersionFromFile("path-to-your-app-assembly");

            var isDebug = IsDebug();
            var netCoreTargetVersion = new Version(3, 1);
            var solutionDirectory = Directory.GetParent(Directory.GetCurrentDirectory()).FullName;
            var sourceDirectoryPath = "my/app/publish/directory"; // directory where your app is published

            var setupProjectDirectoryPath = Directory.GetCurrentDirectory();
            var assetsFolderDirectoryPath = Path.Combine(setupProjectDirectoryPath, "Assets");
            var filesFolderDirectoryPath = Path.Combine(assetsFolderDirectoryPath, "Files");
            var filesProgramMenuDirectoryPath = Path.Combine(filesFolderDirectoryPath, "ProgramMenu");
            var localizationsFolderDirectoryPath = Path.Combine(assetsFolderDirectoryPath, "Localizations");
            var outputInstallerDirectoryPath = Path.Combine(solutionDirectory, "RELEASES");
            var outputInstallerFileName = InstallerOutputFileNamePrefix + version.ToString(3);

            var msiFilePath = new ManagedProject()
                .SetProjectInfo(
                    upgradeCode: ProductUpgradeCode, // unique for this project; same value for all versions; must not be changed between versions.
                    name: ProductFullName,
                    description: ProductDescription,
                    version: version)
                .SetControlPanelInfo(
                    name: ProductFullName,
                    manufacturer: CompanyFullName,
                    readme: ProductHelpUrl,
                    comment: ProductComments,
                    contact: ProductContact,
                    helpUrl: new Uri(ProductHelpUrl),
                    aboutUrl: new Uri(ProductAboutUrl),
                    productIconFilePath: new FileInfo(ProductIconFilePath))
                .DisableDowngradeToPreviousVersion()
                .AddDirectories(
                    // ProgramFiles folder must be specified **first**, only then it will be marked as "INSTALLDIR"
                    new Dir("%ProgramFiles%",
                        new Dir(CompanyFolderName,
                            new InstallDir(ProductFolderName,
                                new Files(
                                    sourcePath: Path.Combine(sourceDirectoryPath, "*.*"),
                                    filter: (filePath) => !filePath.EndsWithAny(true, ".pdb", ".obj", ".plist"))
                                )
                            )
                    ),
                    new Dir("ProgramMenuFolder",
                        new Dir(ProductProgramMenuFolderName,
                            new Files(sourcePath: Path.Combine(filesProgramMenuDirectoryPath, "*.*"))
                        )
                    )
                )
                .AddWindowsServiceAndFirewallRule(
                    executableFileName: ServiceExecutableFileName,
                    name: ServiceName,
                    displayName: ServiceDisplayName,
                    description: ServiceDescription)
                .SignWithCertificateThumprint(
                    certificateThumbprint: CertificateThumbprint,
                    signedContentDescription: ProductFullName,
                    timestampServerUrl: new Uri(CertificateTimestampUrl),
                    hashAlgorithm: HashAlgorithmType.sha256)
                .SetMinimalUI(
                    backgroundImage: new FileInfo(InstallerBackgroundImagePath),
                    bannerImage: new FileInfo(InstallerBannerImagePath),
                    licenceRtfFile: new FileInfo(ProductLicenceRTFFilePath))
                .SetOutputPath(
                    outputDir: new DirectoryInfo(outputInstallerDirectoryPath),
                    outputFileName: outputInstallerFileName)
                .PreserveTempFiles(isDebug)
                // Custom actions
                .BindCustomAction<GreetingsCustomAction>()
                // build the main MSI
                .BuildMultilanguageMsi(
                    new ProjectLocalization("en-US"),
                    new ProjectLocalization(
                        language: "sk-SK",
                        localizationFile: Path.Combine(localizationsFolderDirectoryPath, "WixUI_sk-SK.wxl"),
                        downgradeErrorMessage: "Novšia verzia aplikácie [ProductName] je už na tomto počítači nainštalovaná. Ak si naozaj prajete nainštalovať staršiu verziu, prosím, najskôr odinštalujte túto aplikáciu. Inštalácia teraz skončí."));

            // Bootstrapping (combine msi + its sw prerequisities)
            const string dotNetCoreHostingDetectedVariableName = "DOT_NET_CORE_AND_HOSTING_DETECTED";
            const string dotNetCoreHostingVersionVariableName = "DOT_NET_CORE_AND_HOSTING_VERSION";
            const string win10OrNewerDetectedVariableName = "WINDOWS10_OR_NEWER_DETECTED";
            const string dotNetFrameworkReleaseVersionVariableName = "DOT_NET_FRAMEWORK_RELEASE_VERSION";

            var dotNetCoreHostingDetectedCondition = new WixExpression(dotNetCoreHostingDetectedVariableName);
            var dotNetCoreHostingMinVersionInstalledCondition = WixExpression.Create(
                new WixExpression(dotNetCoreHostingVersionVariableName),
                WixComparativeExpressionOperator.Gte,
                new WixExpression($"\"{netCoreTargetVersion.ToString(2)}\""));
            var dotNetFramework45OrNewerInstalledCondition = WixExpression.Create(
                new WixExpression(dotNetFrameworkReleaseVersionVariableName),
                WixComparativeExpressionOperator.Gte,
                new WixExpression(DotNeFrameworkReleaseMinimumVersion.DotNetFramework45.ToString()));
            var win10OrNewerCondition = new WixExpression(win10OrNewerDetectedVariableName);
            var win8or8dot1Condition = !win10OrNewerCondition & (WixExpression.OsVersion(WixComparativeExpressionOperator.Eq, VersionNT.Windows8OrServer2012) | WixExpression.OsVersion(WixComparativeExpressionOperator.Eq, VersionNT.Windows8dot1OrWindows10OrServer2012R2OrServer2016OrServer2019));
            var win8or8dot1x64Condition = WixExpression.Is64BitOS() & win8or8dot1Condition;
            var win8or8dot1x86Condition = WixExpression.Is32BitOS() & win8or8dot1Condition;
            var win7Condition = WixExpression.OsVersion(WixComparativeExpressionOperator.Eq, VersionNT.Windows7OrWindows7Sp1OrServer2008R2);
            var win7x64Condition = WixExpression.Is64BitOS() & win7Condition;
            var win7x86Condition = WixExpression.Is32BitOS() & win7Condition;

            var bootstrapper = new Bundle()
                .SetInfo(
                    upgradeCode: BootstrapperUpgradeCode, // unique for this bootstrapper; same value for all versions; must not be changed between versions.
                    name: ProductFullName,
                    version: version)
                .PreserveTempFiles(isDebug)
                .SignWithCertificateThumprint(
                    certificateThumbprint: CertificateThumbprint,
                    signedContentDescription: ProductFullName,
                    timestampServerUrl: new Uri(CertificateTimestampUrl),
                    hashAlgorithm: HashAlgorithmType.sha256)
                .HideFromAddRemovePrograms()
                .SuppressApplicationOptionsUI()
                .IncludeWixUtilExtension()

                // request variables used in dependency conditions
                .AddRegistrySearchAspNetCoreExists(dotNetCoreHostingDetectedVariableName)
                .AddRegistrySearchAspNetCoreVersion(dotNetCoreHostingVersionVariableName)
                .AddRegistrySeachWindows10OrNewerDetected(win10OrNewerDetectedVariableName)
                .AddRegistrySearchForDotNetFrameworkReleaseVersion(dotNetFrameworkReleaseVersionVariableName)

                // define dependencies
                .AddOnlineDependency<ExePackage>("NDP452-KB2901907-x86-x64-AllOS-ENU.exe", // install .NET Framework 4.5 if missin on computer
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: !dotNetFramework45OrNewerInstalledCondition)
                .AddOnlineDependency<MsuPackage>("Windows6.1-KB2533623-x64.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win7x64Condition)
                .AddOnlineDependency<MsuPackage>("Windows6.1-KB2533623-x86.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win7x86Condition)
                .AddOnlineDependency<MsuPackage>("Windows6.1-KB2999226-x64.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win7x64Condition)
                .AddOnlineDependency<MsuPackage>("Windows6.1-KB2999226-x86.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win7x86Condition)
                .AddOnlineDependency<MsuPackage>("Windows8.1-KB3118401-x64.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win8or8dot1x64Condition)
                .AddOnlineDependency<MsuPackage>("Windows8.1-KB3118401-x86.msu",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: win8or8dot1x86Condition)
                .AddOnlineDependency<ExePackage>("dotnet-hosting-3.1.3-win.exe",
                    BootstrapperDependenciesDirectoryPath, BootstrapperDependenciesDownloadUrlParentPath,
                    installCondition: !(dotNetCoreHostingDetectedCondition & dotNetCoreHostingMinVersionInstalledCondition),
                    exeExitCodeMap: new ExitCodeMapBuilder().Add(ResultWin32.ERROR_PRODUCT_VERSION, BehaviorValues.success).Build()) // Ignore the "Newer version installed" error
                .AddRollbackBoundary()
                .AddLocalDependency<MsiPackage>(msiFilePath, isRequired: true, msiVisibleInAddRemoveProgramsMenu: true, msiDisplayInternalUI: true);
            
            bootstrapper.Application.LogoFile = BootstrapperLogoFilePath;
            bootstrapper.Application.AddLocalization(new BootstrapperAppLocalization("sk-SK", Path.Combine(BootstrapperAssetsDirectoryPath, "Localizations", "thm.sk-SK.wxl")));

            // building online bundle
            var onlineExeFilePath = msiFilePath.PathChangeExtension(".online.exe");
            bootstrapper.Build(onlineExeFilePath);
            
            // building offline bundle
            var offlineExeFilePath = msiFilePath.PathChangeExtension(".offline.exe");
            bootstrapper
                .SetAllOnlineDependenciesToLocal()
                .Build(offlineExeFilePath);
        }

        private static bool IsDebug()
        {
#if DEBUG
            return true;
#else
            return false;
#endif
        }
    }

    class GreetingsCustomAction : CustomAction, ILoadAware, IBeforeInstallAware, IAfterInstallAware
    {
        public void OnLoad(SetupEventArgs e)
        {
            MessageBox.Show("Hello from OnLoad event handler!");
        }

        public void OnBeforeInstallFiles(SetupEventArgs e)
        {
            MessageBox.Show("Hello from Before install event handler!");
        }

        public void AfterInstallFiles(SetupEventArgs e)
        {
            MessageBox.Show("Hello from After install event handler!");
        }
    }
}

Content of WixUI_sk-SK.wxl file:

<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="sk-SK" Codepage="1250" xmlns="http://schemas.microsoft.com/wix/2006/localization">
    <String Id="msierrFirewallCannotConnect" Overridable="yes">Cannot connect to Windows Firewall.  ([2]   [3]   [4]   [5])</String>
    <String Id="WixSchedFirewallExceptionsInstall" Overridable="yes">Configuring Windows Firewall</String>
    <String Id="WixSchedFirewallExceptionsUninstall" Overridable="yes">Configuring Windows Firewall</String>
    <String Id="WixRollbackFirewallExceptionsInstall" Overridable="yes">Rolling back Windows Firewall configuration</String>
    <String Id="WixExecFirewallExceptionsInstall" Overridable="yes">Installing Windows Firewall configuration</String>
    <String Id="WixRollbackFirewallExceptionsUninstall" Overridable="yes">Rolling back Windows Firewall configuration</String>
    <String Id="WixExecFirewallExceptionsUninstall" Overridable="yes">Uninstalling Windows Firewall configuration</String>
</WixLocalization>

Content of thm.sk-SK.wxl file:

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->

<WixLocalization Culture="sk-sk" Language="1051" xmlns="http://schemas.microsoft.com/wix/2006/localization">
  <String Id="Caption">[WixBundleName] – inštalácia</String>
  <String Id="Title">[WixBundleName]</String>
  <String Id="InstallHeader">Welcome</String>
  <String Id="InstallMessage">Setup will install [WixBundleName] on your computer. Click install to continue, options to set the install directory or Close to exit.</String>
  <String Id="InstallVersion">Version [WixBundleVersion]</String>
  <String Id="ConfirmCancelMessage">Naozaj chcete zrušiť operáciu?</String>
  <String Id="ExecuteUpgradeRelatedBundleMessage">Previous version</String>
  <String Id="HelpHeader">Pomocník pre inštaláciu</String>
  <String Id="HelpText">/install | /repair | /uninstall | /layout [directory] - nainštaluje, opraví, odinštaluje
  alebo vytvorí kompletnú lokálnu kópiu balíčka v adresári. Predvolená je možnosť Install.

/passive | /quiet – zobrazí minimálne používateľské rozhranie bez výziev alebo
  nezobrazí žiadne používateľské rozhranie ani výzvy. Predvolene sa
  zobrazuje používateľské rozhranie aj všetky výzvy.

/norestart   – zruší všetky pokusy o reštart. Používateľské rozhranie
  predvolene zobrazí pred reštartom výzvu.

/log log.txt – urobí záznam do určeného súboru. Súbor denníka sa predvolene
  vytvorí v priečinku %TEMP%.</String>
  <String Id="HelpCloseButton">&amp;Zavrieť</String>
  <String Id="InstallLicenseLinkText">[WixBundleName] &lt;a href="#"&gt;Licenčná zmluva&lt;/a&gt;.</String>
  <String Id="InstallAcceptCheckbox">Súhlasím s podmienkami licenčnej zmluvy</String>
  <String Id="InstallOptionsButton">&amp;Možnosti</String>
  <String Id="InstallInstallButton">&amp;Inštalovať</String>
  <String Id="InstallCloseButton">&amp;Zavrieť</String>
  <String Id="OptionsHeader">Možnosti inštalácie</String>
  <String Id="OptionsLocationLabel">Cieľový adresár:</String>
  <String Id="OptionsBrowseButton">&amp;Prehľadávať</String>
  <String Id="OptionsOkButton">&amp;OK</String>
  <String Id="OptionsCancelButton">&amp;Zrušiť</String>
  <String Id="ProgressHeader">Priebeh inštalácie</String>
  <String Id="ProgressLabel">Spracúva sa:</String>
  <String Id="OverallProgressPackageText">Spracúva sa...</String>
  <String Id="ProgressCancelButton">&amp;Zrušiť</String>
  <String Id="ModifyHeader">Oprava inštalácie</String>
  <String Id="ModifyRepairButton">&amp;Opraviť</String>
  <String Id="ModifyUninstallButton">&amp;Odinštalovať</String>
  <String Id="ModifyCloseButton">&amp;Zavrieť</String>
  <String Id="SuccessRepairHeader">Oprava úspešne dokončená</String>
  <String Id="SuccessUninstallHeader">Odinštalácia úspešne dokončená</String>
  <String Id="SuccessInstallHeader">Inštalácia úspešne dokončená</String> 
  <String Id="SuccessHeader">Inštalácia </String>  
  <String Id="SuccessLaunchButton">&amp;Spustiť</String>
  <String Id="SuccessRestartText">Pred použitím programu je potrebné reštartovať počítač.</String>
  <String Id="SuccessRestartButton">&amp;Reštartovať</String>
  <String Id="SuccessCloseButton">&amp;Dokončiť</String>
  <String Id="FailureHeader">Inštalácia zlyhala</String>
  <String Id="FailureInstallHeader">Inštalácia zlyhala</String>
  <String Id="FailureUninstallHeader">Odinštalácia zlyhala</String>
  <String Id="FailureRepairHeader">Oprava zlyhala</String> 
  <String Id="FailureHyperlinkLogText">Inštalácia zlyhala pre jednu alebo viac príčin. Odstráňte problémy a skúste znova spustiť inštaláciu. Ďalšie informácie nájdete v &lt;a href="#"&gt;súbore denníka&lt;/a&gt;.</String>
  <String Id="FailureRestartText">Dokončenie všetkých zmien softvéru vyžaduje reštart počítača.</String>
  <String Id="FailureRestartButton">&amp;Reštartovať</String>
  <String Id="FailureCloseButton">&amp;Zavrieť</String>
  <String Id="FilesInUseHeader">Používané spbory</String>
  <String Id="FilesInUseLabel">Nasledujúce aplikácie používajú súbory, ktoré musí táto inštalácia aktualizovať:</String>
  <String Id="FilesInUseCloseRadioButton">Zavrieť &amp;aplikácie a pokúsiť sa ich reštartovať neskôr.</String>
  <String Id="FilesInUseDontCloseRadioButton">&amp;Nezatvárať aplikácie. Bude potrebný reštart systému.</String>
  <String Id="FilesInUseOkButton">&amp;OK</String>
  <String Id="FilesInUseCancelButton">&amp;Zrušiť</String>
  <String Id="ErrorFailNoActionReboot">Žiadna akcia nebola vykonaná, pretože je potrebný reštart systému.</String>
</WixLocalization>

Application will produce following files:

File name Original source
my-app-v1.0.0.msi MSI product installer that will install only your app on the machine, without dependencies (.net core and windows KB's)
my-app-v1.0.0.offline.exe Bootstrapper application that installs all software dependencies and your product. Dependencies are embedded, no internet connection is required during installation.
my-app-v1.0.0.online.exe Bootstrapper application that installs all software dependencies and your product. Dependencies are downloaded from online location, if installing condition is met.
Clone this wiki locally