Host .NET in Windows Service
- 1. .NET Generic Host
- 2. Create Windows Service using BackgroundService
- 3. Host ASP.NET Core in a Windows Service
- 4. Create a Windows Service installer
- References
1. .NET Generic Host
The .NET Generic Host, available in the Microsoft.Extensions.Hosting NuGet package, is responsible for app startup and lifetime management. A host is an object that encapsulates an app’s resources and lifetime functionality, such as: [1] [2]
-
IHostedService implementations
When a host starts, it calls IHostedService.StartAsync
on each implementation of IHostedService
registered in the service container’s collection of hosted services. In a worker service app, all IHostedService
implementations that contain BackgroundService
instances have their BackgroundService.ExecuteAsync
methods called.
The main reason for including all of the app’s interdependent resources in one object is lifetime management: control over app startup and graceful shutdown.
1.1. Set up a host
<!-- Example.WorkerService.csproj -->
<!-- <Project Sdk="Microsoft.NET.Sdk.Worker"> -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>
// Program.cs
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
using IHost host = builder.Build();
host.Run();
public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
logger.LogInformation("{}: I'm busy at work.", DateTime.Now.TimeOfDay);
}
}
}
$ dotnet run
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
. . .
info: Worker[0]
11:17:39.1114826: I'm busy at work.
info: Worker[0]
11:17:44.1725394: I'm busy at work.
info: Worker[0]
11:17:49.1739304: I'm busy at work.
^Cinfo: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
For an HTTP workload:
<!-- Example.WebApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
// Program.cs
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
$ ASPNETCORE_URLS=http://+:5000 dotnet run --no-launch-profile
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
. . .
^Cinfo: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
$ curl -i localhost:5000
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Tue, 02 Apr 2024 03:44:23 GMT
Server: Kestrel
Transfer-Encoding: chunked
Hello World!
1.2. Kestrel server
Kestrel server is the default, cross-platform HTTP server implementation. Kestrel provides the best performance and memory utilization, but it doesn’t have some of the advanced features in HTTP.sys. [3]
Use Kestrel:
-
By itself as an edge server processing requests directly from a network, including the Internet.
-
With a reverse proxy server, such as Internet Information Services (IIS), Nginx, or Apache. A reverse proxy server receives HTTP requests from the Internet and forwards them to Kestrel.
Either hosting configuration—with or without a reverse proxy server—is supported.
For Kestrel configuration guidance and information on when to use Kestrel in a reverse proxy configuration, see Kestrel web server in ASP.NET Core.
1.3. Use HTTP/3 with the ASP.NET Core Kestrel web server
HTTP/3 has different requirements depending on the operating system. If the platform that Kestrel is running on doesn’t have all the requirements for HTTP/3, then it’s disabled, and Kestrel will fall back to other HTTP protocols.
-
Windows
-
Windows 11 Build 22000 or later OR Windows Server 2022.
-
TLS 1.3 or later connection.
-
-
Linux
-
libmsquic
package installed.libmsquic
is published via Microsoft’s official Linux package repository atpackages.microsoft.com
..NET 6 is only compatible with the 1.9.x versions of libmsquic. Libmsquic 2.x is not compatible due to breaking changes. Libmsquic receives updates to 1.9.x when needed to incorporate security fixes. -
macOS
HTTP/3 isn’t currently supported on macOS and may be available in a future release.
$ apt-cache madison libmsquic
libmsquic | 2.3.5 | https://packages.microsoft.com/debian/12/prod bookworm/main amd64 Packages
libmsquic | 2.3.4 | https://packages.microsoft.com/debian/12/prod bookworm/main amd64 Packages
. . .
$ sudo apt-get install libmsquic -y
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
libnuma1
The following NEW packages will be installed:
libmsquic libnuma1
0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded.
. . .
$ dpkg -S libmsquic
libmsquic: /usr/share/doc/libmsquic
libmsquic: /usr/lib/x86_64-linux-gnu/libmsquic.so.2.3.5
libmsquic: /usr/lib/x86_64-linux-gnu/libmsquic.lttng.so.2.3.5
libmsquic: /usr/lib/x86_64-linux-gnu/libmsquic.so.2
libmsquic: /usr/share/doc/libmsquic/changelog.gz
$ ASPNETCORE_URLS=https://+:5001 dotnet run \
--no-launch-profile \
--Kestrel:EndpointDefaults:Protocols=Http1AndHttp2AndHttp3
warn: Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer[8]
The ASP.NET Core developer certificate is not trusted. For information about trusting the ASP.NET Core devel
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://[::]:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
$ docker run -it --rm --network host ymuski/curl-http3 curl -ik --http3 https://localhost:5001
HTTP/3 200
content-type: text/plain; charset=utf-8
date: Tue, 02 Apr 2024 06:19:53 GMT
server: Kestrel
alt-svc: h3=":5001"; ma=86400
Hello World!
2. Create Windows Service using BackgroundService
To interop with native Windows Services from .NET IHostedService
implementations, it’s needed to install the Microsoft.Extensions.Hosting.WindowsServices NuGet package. [5]
The To log events lower than
The following code changes the
|
2.1. Update project file
<!-- Example.WorkerService.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<DebugType>embedded</DebugType>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="Microsoft.Extensions.Logging" />
<Using Include="Microsoft.Extensions.Logging.Configuration" />
<Using Include="Microsoft.Extensions.Logging.EventLog" />
</ItemGroup>
</Project>
2.2. Rewrite the Program
class
// Program.cs
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = ".NET Example WorkerService";
});
builder.Logging.AddEventLog(options =>
{
options.SourceName = ".NET Example WorkerService";
});
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
builder.Services.AddHostedService<Worker>();
using IHost host = builder.Build();
host.Run();
public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
logger.LogInformation("{}: I'm busy at work.", DateTime.Now.TimeOfDay);
}
}
catch (OperationCanceledException)
{
// When the stopping token is canceled, for example, a call made from services.msc,
// we shouldn't exit with a non-zero exit code. In other words, this is expected...
}
catch (Exception ex)
{
logger.LogError(ex, "{Message}", ex.Message);
// Terminates this process and returns an exit code to the operating system.
// This is required to avoid the 'BackgroundServiceExceptionBehavior', which
// performs one of two scenarios:
// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
// 2. When set to "StopHost": will cleanly stop the host, and log errors.
//
// In order for the Windows Service Management system to leverage configured
// recovery options, we need to terminate the process with a non-zero exit code.
Environment.Exit(1);
}
}
}
2.3. Publish the app
To create the .NET Worker Service app as a Windows Service, it’s recommended that you publish the app as a single file executable. It’s less error-prone to have a self-contained executable, as there aren’t any dependent files lying around the file system. But you may choose a different publishing modality, which is perfectly acceptable, so long as you create an *.exe
file that can be targeted by the Windows Service Control Manager.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<DebugType>embedded</DebugType>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
. . .
$ dotnet publish (1)
MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET
Restore complete (0.3s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
Example.WorkerService succeeded (2.1s) → bin\Release\net8.0-windows\win-x64\publish\
Build succeeded in 2.6s
$ ls bin/Release/net8.0-windows/win-x64/publish/
Example.WorkerService.exe
1 | The dotnet publish command now uses the Release configuration instead of the Debug configuration by default if the target framework is .NET 8 or a later version. |
You can also publish .NET apps using the .NET CLI with the following switches.
|
2.4. Create the Windows Service
To create a Windows Service, run PowerShell as an Administrator.
New-Service -Name "TestService" -BinaryPathName 'C:\Path\To\App.WindowsService.exe'
Let’s create a directory, and copy the executable file to it.
> mkdir D:\Example.WorkerService\
> cp .\bin\Release\net8.0-windows\win-x64\publish\Example.WorkerService.exe D:\Example.WorkerService\
-
Create the
.NET Example WorkerService
serviceNew-Service -Name ".NET Example WorkerService" -BinaryPathName D:\Example.WorkerService\Example.WorkerService.exe
-
Start the
.NET Example WorkerService
serviceStart-Service -Name ".NET Example WorkerService"
-
Get the status of the
.NET Example WorkerService
serviceGet-Service -Name ".NET Example WorkerService" | Format-List
-
Get events from the
.NET Example WorkerService
serviceGet-EventLog -LogName Application -Source ".NET Example WorkerService" | Format-List
Index : 3884 EntryType : Warning InstanceId : 0 Message : Category: Worker EventId: 0 16:15:27.1390426: I'm busy at work. Category : (0) CategoryNumber : 0 ReplacementStrings : {Category: Worker EventId: 0 16:15:27.1390426: I'm busy at work. } Source : .NET Example WorkerService TimeGenerated : 04/02/2024 16:15:27 TimeWritten : 04/02/2024 16:15:27 UserName : Index : 3883 EntryType : Information InstanceId : 0 Message : Service started successfully. Category : (0) CategoryNumber : 0 ReplacementStrings : {Service started successfully.} Source : .NET Example WorkerService TimeGenerated : 04/02/2024 16:15:22 TimeWritten : 04/02/2024 16:15:22 UserName :
-
Stop the
.NET Example WorkerService
serviceStop-Service -Name ".NET Example WorkerService"
-
Remove the
.NET Example WorkerService
serviceRemove-Service -Name ".NET Example WorkerService"
The Remove-Service
cmdlet was introduced in PowerShell 6.0.Use the native Windows Service Control Manager’s (
sc.exe
) delete command.sc.exe delete ".NET Example WorkerService"
3. Host ASP.NET Core in a Windows Service
An ASP.NET Core app can be hosted on Windows as a Windows Service without using IIS. When hosted as a Windows Service, the app automatically starts after server reboots. [6]
3.1. Current directory and content root
The current working directory returned by calling GetCurrentDirectory
for a Windows Service is the C:\WINDOWS\system32
folder. The system32
folder isn’t a suitable location to store a service’s files (for example, settings files). Use one of the following approaches to maintain and access a service’s assets and settings files.
-
Use
IHostEnvironment.ContentRootPath
orContentRootFileProvider
to locate an app’s resources. -
When the app runs as a service, sets the
ContentRootPath
toAppContext.BaseDirectory
. -
Don’t attempt to use
GetCurrentDirectory
to obtain a resource path because a Windows Service app returns theC:\WINDOWS\system32
folder as its current directory.
3.2. Configure endpoints
New ASP.NET Core projects are configured to bind to a random HTTP port between 5000-5300 and a random HTTPS port between 7000-7300. The selected ports are stored in the generated Properties/launchSettings.json
file and can be modified by the developer. The launchSetting.json
file is only used in local development.
If there’s no endpoint configuration, then Kestrel binds to http://localhost:5000
.
For additional URL and port configuration approaches, see Configure endpoints for the ASP.NET Core Kestrel web server.
3.3. Update project file
<!-- Example.WebApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
<DebugType>embedded</DebugType>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>
</Project>
3.4. Rewrite the Program
class
// Program.cs
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
// See https://github.com/dotnet/AspNetCore.Docs/issues/23387#issuecomment-927317675
WebApplicationOptions options = new()
{
Args = args,
// Sets the content root to AppContext.BaseDirectory.
ContentRootPath = WindowsServiceHelpers.IsWindowsService() ? AppContext.BaseDirectory : default
};
WebApplicationBuilder builder = WebApplication.CreateBuilder(options);
// Sets the host lifetime to WindowsServiceLifetime.
builder.Services.AddWindowsService(options =>
{
options.ServiceName = ".NET Example WebApp";
});
builder.Logging.AddEventLog(options =>
{
options.SourceName = ".NET Example WebApp";
});
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
WebApplication app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
3.5. Publish the app
$ dotnet.exe publish
MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET
Restore complete (0.4s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
Example.WebApp succeeded (0.4s) → bin\Release\net8.0-windows\win-x64\publish\
Build succeeded in 0.8s
$ ls bin/Release/net8.0-windows/win-x64/publish/
Example.WebApp.exe appsettings.Development.json appsettings.json
3.6. Create the Windows Service
-
Create work directory and copy files
> mkdir D:\Example.WebApp\ > cp .\bin\Release\net8.0-windows\win-x64\publish\Example.WebApp.exe D:\Example.WebApp\
-
Create the
.NET Example WebApp
serviceNew-Service -Name ".NET Example WebApp" -BinaryPathName D:\Example.WebApp\Example.WebApp.exe
Status Name DisplayName ------ ---- ----------- Stopped .NET Example We... .NET Example WebApp
-
Start the
.NET Example WebApp
serviceStart-Service -Name ".NET Example WebApp"
-
Get the status of the
.NET Example WebApp
serviceGet-Service -Name ".NET Example WebApp" | Format-List
Name : .NET Example WebApp DisplayName : .NET Example WebApp Status : Running DependentServices : {} ServicesDependedOn : {} CanPauseAndContinue : False CanShutdown : True CanStop : True ServiceType : Win32OwnProcess
-
Get the events of the
.NET Example WebApp
serviceGet-EventLog -LogName Application -Source ".NET Example WebApp"
Index Time EntryType Source InstanceID Message ----- ---- --------- ------ ---------- ------- 4677 Apr 02 17:39 Information .NET Example WebApp 0 Service started successfully.
-
Test the endpoint of the
.NET Example WebApp
serviceInvoke-WebRequest -Uri http://localhost:5000
StatusCode : 200 StatusDescription : OK Content : Hello World! RawContent : HTTP/1.1 200 OK Transfer-Encoding: chunked Content-Type: text/plain; charset=utf-8 Date: Tue, 02 Apr 2024 09:33:34 GMT Server: Kestrel Hello World! . . .
-
Stop and delete the
.NET Example WebApp
serviceStop-Service -Name ".NET Example WebApp"
sc.exe delete ".NET Example WebApp"
3.7. Static files in ASP.NET Core
Static files, such as HTML, CSS, images, and JavaScript, are assets an ASP.NET Core app serves directly to clients by default, which are stored within the project’s web root directory. For more information, see Static files in ASP.NET Core. [7]
3.7.1. Create the wwwroot
directory, and sample files
mkdir wwwroot
Write-Output "Hello Default Files!" | Out-File -Encoding ascii .\wwwroot\index.html
Write-Output "Hello Windows Service!" | Out-File -Encoding ascii .\wwwroot\service.html
3.7.2. Rewrite the Program
class
// Program.cs
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;
// See https://github.com/dotnet/AspNetCore.Docs/issues/23387#issuecomment-927317675
WebApplicationOptions options = new()
{
Args = args,
// Sets the content root to AppContext.BaseDirectory.
ContentRootPath = WindowsServiceHelpers.IsWindowsService() ? AppContext.BaseDirectory : default
};
WebApplicationBuilder builder = WebApplication.CreateBuilder(options);
// Sets the host lifetime to WindowsServiceLifetime.
builder.Services.AddWindowsService(options =>
{
options.ServiceName = ".NET Example WebApp";
});
builder.Logging.AddEventLog(options =>
{
options.SourceName = ".NET Example WebApp";
});
LoggerProviderOptions.RegisterProviderOptions<EventLogSettings, EventLogLoggerProvider>(builder.Services);
WebApplication app = builder.Build();
app.UseFileServer();
// Set the path to `/hello`, instead of the root `/`.
app.MapGet("/hello", () => "Hello World!");
app.Run();
3.7.3. Publish the app
$ dotnet publish
MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET
Restore complete (0.7s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
Example.WebApp succeeded (4.7s) → bin\Release\net8.0-windows\win-x64\publish\
Build succeeded in 5.5s
$ ls bin/Release/net8.0-windows/win-x64/publish/
Example.WebApp.exe appsettings.Development.json appsettings.json wwwroot
3.7.4. Update the Windows Service
-
Stop the
.NET Example WebApp
serviceStop-Service -Name ".NET Example WebApp"
-
Copy files
Copy-Item ` -Path .\bin\Release\net8.0-windows\win-x64\publish\* ` -Destination D:\Example.WebApp\ ` -Recurse ` -Force
-
Start the
.NET Example WebApp
serviceStart-Service -Name ".NET Example WebApp"
-
Test the endpoint of the
.NET Example WebApp service
> Invoke-WebRequest -Uri http://localhost:5000 ` | Select-Object -Property StatusCode,StatusDescription,Content ` | Format-List StatusCode : 200 StatusDescription : OK Content : Hello Default Files! > Invoke-WebRequest -Uri http://localhost:5000/index.html ` | Select-Object -Property StatusCode,StatusDescription,Content ` | Format-List StatusCode : 200 StatusDescription : OK Content : Hello Default Files! > Invoke-WebRequest -Uri http://localhost:5000/service.html ` | Select-Object -Property StatusCode,StatusDescription,Content ` | Format-List StatusCode : 200 StatusDescription : OK Content : Hello Windows Service! > Invoke-WebRequest -Uri http://localhost:5000/hello ` | Select-Object -Property StatusCode,StatusDescription,Content ` | Format-List StatusCode : 200 StatusDescription : OK Content : Hello World!
4. Create a Windows Service installer
An Windows Installer, an installation and configuration service provided with Windows, bundles the app’s executables and exposes a customizable installation user experience. [8]
The Wix Toolset is a set of tools that build Windows installation packages from XML source code.
The WiX Toolset only supports Windows. |
The following steps will use the Example.WebApp
project as an example to package a Windows Installer.
4.1. WiX Command-line .NET tool
WiX, available as a .NET tool, supports commands to perform particular operations. For example, the build
command can be used to build MSI packages, bundles, and other package types.
-
Install the Wix Toolset
-
Create the local tool manifest file
> dotnet new tool-manifest The template "Dotnet local tool manifest file" was created successfully.
-
Install the Wix Toolset
> dotnet tool install wix --version 5.0.0 You can invoke the tool from this directory using the following commands: 'dotnet tool run wix' or 'dotnet wix'. Tool 'wix' (version '5.0.0') was successfully installed. . . .
-
Create the WiX package source file
Package.wxs
<?xml version="1.0" encoding="UTF-8"?> <?define Name = ".NET Example WebApp" ?> <?define Manufacturer = ".NET Example Corporation" ?> <?define Version = "1.0.0.0" ?> <?define UpgradeCode = "288C8793-D5D7-427F-A82F-B647ECDBDCC1" ?> <?define ServiceName = ".NET Example WebApp" ?> <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"> <Package Name="$(Name)" Manufacturer="$(Manufacturer)" Version="$(Version)" (1) UpgradeCode="$(var.UpgradeCode)" Compressed="true"> <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." /> (2) <MediaTemplate EmbedCab="yes" /> <StandardDirectory Id="ProgramFiles64Folder"> <Directory Id="ROOTDIRECTORY" Name="!(bind.Property.Manufacturer)"> <Directory Id="INSTALLFOLDER" Name="!(bind.Property.ProductName)"> <Directory Id="WEBROOTDIRECTORY" Name="wwwroot" /> </Directory> </Directory> </StandardDirectory> <ComponentGroup Id="WebAppServiceComponents" Directory="INSTALLFOLDER"> <Component Id="ServiceExecutable" Bitness="always64"> <File Source="$(var.Example.WebApp.TargetDir)publish\Example.WebApp.exe" /> <ServiceInstall Id="ServiceInstaller" Type="ownProcess" Name="$(ServiceName)" DisplayName="$(ServiceName)" Description="A joke service that periodically logs nerdy humor." Start="auto" ErrorControl="normal" /> <ServiceControl Id="StartService" Start="install" Stop="both" Remove="uninstall" Name="$(ServiceName)" Wait="true" /> </Component> </ComponentGroup> <ComponentGroup Id="AppSettingsComponents" Directory="INSTALLFOLDER"> <File Source="$(var.Example.WebApp.TargetDir)publish\appsettings.json" /> <Files Include="$(var.Example.WebApp.TargetDir)publish\appsettings.*.json" /> </ComponentGroup> <ComponentGroup Id="WebRootComponents" Directory="INSTALLFOLDER"> <Files Directory="WEBROOTDIRECTORY" Include="$(var.Example.WebApp.TargetDir)publish\wwwroot\**" /> </ComponentGroup> <Feature Id="WebApp"> <ComponentGroupRef Id="WebAppServiceComponents"/> <ComponentGroupRef Id="AppSettingsComponents" /> <ComponentGroupRef Id="WebRootComponents" /> </Feature> </Package> </Wix>
1 Windows Installer uses only the first three fields of the product version. See ProductVersion Property for descriptions of these fields. If you include a fourth field in your product version, the installer ignores the fourth field. To create an upgrade installer package, you can update the
version
and repackage it.2 During a major upgrade using Windows Installer, the installer searches the user’s computer for applications that are related to the pending upgrade, and when it detects one, it retrieves the version of the installed application from the system registry. The installer then uses information in the upgrade database to determine whether to upgrade the installed application. -
Publish the
Example.WebApp
project.> dotnet publish MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET Restore complete (0.2s) You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy Example.WebApp succeeded (1.2s) → bin\Release\net8.0-windows\win-x64\publish\ Build succeeded in 1.6s
-
Build the MSI installer
> $outdir = dotnet msbuild -getProperty:OutDir -p:Configuration=Release > dotnet wix build -arch x64 Package.wxs -d var.Example.WebApp.TargetDir=$outdir -out Example.WebApp.msi
> dotnet msbuild -getProperty:OutDir -p:Configuration=Release bin\Release\net8.0-windows\win-x64\
-
Test in Windows Sandbox
Enabling Windows SandboxWindows Sandbox comes with Windows but isn’t installed by default. The documentation tells you to search from the taskbar for
Turn Windows Features on or off
to bring up the feature list. Another way to get to that list is to visit our old friend ARP (Programs and Features) and click on the Turn Windows Features on or off link on the left.That brings up a list of features. Scroll to almost the bottom of the list and you’ll see
Windows Sandbox
. If you do NOT see it, that means your machine isn’t modern enough to satisfy Windows’s cravings for the newest CPUs. If it’s there and checked, you’re all done. If it’s there and unchecked, check it and click OK. You’ll have to go through a reboot and spinner churn as Sandbox is installed.Once it’s installed, search for
sandbox
and choose theWindows Sandbox
shortcut. The first time you run it, it might take a little while to come up (newbie jitters) but soon enough, you’ll have a window with a familiar-looking Windows desktop.-
Select
Example.WebApp.msi
and pressCtrl+C
(or chooseCopy
from the context menu). Go to your running Windows Sandbox and pressCtrl+V
(orPaste
) on the desktop. -
Double click the
Example.WebApp.msi
to install the.NET Example WebApp
service. You need to run the installation as an administrator.Once the service is installed, you can open
Services
to see the service running. To uninstall the service, use theWindows Add or Remove Programs
feature to call the installer. -
Run the following command on PowerShell to test the service.
> Invoke-WebRequest -Uri http://localhost:5000 ` | Select-Object -Property StatusCode,StatusDescription,Content ` | Format-List StatusCode : 200 StatusDescription : OK Content : Hello Default Files!
-
4.2. WiX MSBuild on the command line and CI/CD build systems
WiX, available as an MSBuild SDK for building from the command line using dotnet build
from the .NET SDK or the .NET Framework-based MSBuild
from Visual Studio, have smart defaults that make for simple .wixproj
project authoring. For example, here’s a minimal .wixproj
that builds an MSI from the .wxs
source files in the project directory:
<Project Sdk="WixToolset.Sdk/5.0.0">
</Project>
FireGiant has released HeatWave Community Edition, available free of charge, to support WiX SDK-style MSBuild projects in Visual Studio. |
-
Create the WiX SDK-style project directory
> mkdir .\src\Example.WebApp.Installer
-
Create the WiX SDK-style project file
> ni .\src\Example.WebApp.Installer\Example.WebApp.Installer.wixproj
<!-- Example.WebApp.Installer.wixproj --> <Project Sdk="WixToolset.Sdk/5.0.0"> <ItemGroup> <ProjectReference Include="..\Example.WebApp\Example.WebApp.csproj" /> </ItemGroup> </Project>
-
Add the WiX SDK-style project to the solution
> dotnet sln add .\src\Example.WebApp.Installer\
-
Copy the
Package.wxs
from the projectExample.WebApp
toExample.WebApp.Installer
> cp .\src\Example.WebApp\Package.wxs .\src\Example.WebApp.Installer\
-
Publish the
Example.WebApp
and build the MSI package> dotnet publish .\src\Example.WebApp\ MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET Restore complete (0.6s) You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy Example.WebApp succeeded (7.5s) → src\Example.WebApp\bin\Release\net8.0-windows\win-x64\publish\ Build succeeded in 8.3s > dotnet build -r win-x64 -c Release -p:InstallerPlatform=x64 .\src\Example.WebApp.Installer\ MSBuild version 17.10.0-preview-24101-01+07fd5d51f for .NET Restore complete (0.4s) You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy Example.WebApp succeeded (0.3s) → src\Example.WebApp\bin\Release\net8.0-windows\win-x64\Example.WebApp.dll Example.WebApp.Installer succeeded (0.0s) → src\Example.WebApp.Installer\bin\Release\Example.WebApp.Installer.msi Build succeeded in 0.9s
4.3. Signing Windows Installer MSI packages
Windows Installer packages can be signed directly by signing tools like Signtool.exe. [9]
SignTool is available as part of the Windows SDK, which you can download from Windows SDK. The tool is installed in the \bin
folder of the SDK installation path, for example: C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe
. [10] [11]
-
Use
New-SelfSignedCertificate
to create a self-signed certificate for testingNew-SelfSignedCertificate ` -Type Custom ` -Subject "CN=Contoso Software, O=Contoso Corporation, C=US" ` -KeyUsage DigitalSignature ` (1) -FriendlyName "Your friendly name goes here" ` -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") ` (2) -CertStoreLocation "Cert:\CurrentUser\My"
1 KeyUsage
: This parameter defines what the certificate may be used for. For a self-signing certificate, this parameter should be set toDigitalSignature
.2 TextExtension
: This parameter includes settings for the following extensions:Extended Key Usage (EKU)
: This extension indicates additional purposes for which the certified public key may be used. For a self-signing certificate, this parameter should include the extension string "2.5.29.37={text}1.3.6.1.5.5.7.3.3", which indicates that the certificate is to be used for code signing.Basic Constraints
: This extension indicates whether or not the certificate is a Certificate Authority (CA). For a self-signing certificate, this parameter should include the extension string "2.5.29.19={text}", which indicates that the certificate is an end entity (not a CA). -
You can view your certificate in a PowerShell window by using the following commands:
Get-ChildItem Cert:\CurrentUser\My | Format-Table Subject, FriendlyName, Thumbprint
-
Export a certificate
To export the certificate in the local store to a Personal Information Exchange (PFX) file, use the
Export-PfxCertificate
cmdlet.When using
Export-PfxCertificate
, you must either create and use a password or use the "-ProtectTo" parameter to specify which users or groups can access the file without a password. Note that an error will be displayed if you don’t use either the "-Password" or "-ProtectTo" parameter.-
Password usage
$password = ConvertTo-SecureString -String <Your Password> -Force -AsPlainText Export-PfxCertificate ` -cert "Cert:\CurrentUser\My\<Certificate Thumbprint>" ` -FilePath <FilePath>.pfx ` -Password $password
-
ProtectTo usage
Export-PfxCertificate ` -cert Cert:\CurrentUser\My\<Certificate Thumbprint> ` -FilePath <FilePath>.pfx ` -ProtectTo <Username or group name>
> Get-ChildItem Cert:\CurrentUser\My | Format-Table Subject, FriendlyName, Thumbprint Subject FriendlyName Thumbprint ------- ------------ ---------- CN=localhost ASP.NET Core HTTPS development certificate 621654A0E5B9683E1129FE5D0871FAB5995F30E6 CN=Contoso Software, O=Contoso Corporation, C=US Your friendly name goes here 532554756AC84D83A274B094A76A8C38FE821227 > $password = ConvertTo-SecureString -String "<Your Password>" -Force -AsPlainText > Export-PfxCertificate -Cert Cert:\CurrentUser\My\532554756AC84D83A274B094A76A8C38FE821227 -FilePath cert.pfx -Password $password . . . Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 04/08/2024 17:01 2782 cert.pfx
-
-
Sign MSI package using SignTool
> signtool sign /v /fd SHA256 /f cert.pfx /p "<Your Password>" Example.WebApp.Installer.msi The following certificate was selected: Issued to: Contoso Software Issued by: Contoso Software Expires: Tue Apr 08 16:54:26 2025 SHA1 hash: 532554756AC84D83A274B094A76A8C38FE821227 Done Adding Additional Store Successfully signed: .\Example.WebApp.Installer.msi Number of files successfully Signed: 1 Number of warnings: 0 Number of errors: 0
References
-
[1] https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host
-
[2] https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host
-
[3] https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/
-
[4] https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/http3
-
[5] https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service
-
[6] https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/windows-service
-
[7] https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files
-
[8] https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service-with-installer
-
[10] https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool
-
[11] https://codesigningstore.com/digitally-sign-msi-installers-with-code-signing