Managing a .NET Service with Blazor, on Windows and Linux

Jacob Duijzer
Team Rockstars IT
Published in
6 min readJul 27, 2022

--

Just recently I had to work on an application which collects data from external systems, processes it and sends it to a central database. It is a WinForms application with a few screens for configurations and logging. I was looking for different approaches to create a new version for the scraper and dived into the options of using a Windows Service.

I forgot where the exact idea came from, but at a certain point I thought: what if I can use Blazor as a UI for the Windows Service. Then I could configure, start and stop, basically do all kind of things with this service with a graphical interface. I read something about systemd services with .NET too, so I could even create a cross-platform version (not that there is any need for Linux, but a I use Linux my self I would love to try that too).

Well, after a few hours, I got it working, and in this post I will show you how to create a cross-platform service that can be installed on Windows as as service and on Linux as a systemd component and be managed with Blazor.

There is a lot of information on how to run a .NET project as a service, on Windows and on Linux (Mac is not supported yet, as far as I know). I will provide a sample project on GitHub and will only show some of the basics here.

The most interesting part for me is hosting a Blazor Server application with Kestrel as a service on both Windows and Linux. This gives endless possibilities in managing the service, not even from the system itself, but also remotely.

Managing service with Blazor

First, we create a normal Blazor Server project. I keep the project as-is and just add a few classes to demonstrate the use of Blazor in the service.

Adding the background service

Create a class called CustomBackgroundService. I use the BackgroundService as a base class, but I could also implement IHostedService. More information about the different types can be found here.

This service is just logging and then waiting for 5 seconds to simulate a process that runs for a while:

public class CustomBackgroundService : BackgroundService
{
public bool IsRunning { get; set; }
private readonly ILogger<CustomBackgroundService> _logger; public CustomBackgroundService(ILogger<CustomBackgroundService> logger) => _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation($"{nameof(CustomBackgroundService)} starting {nameof(ExecuteAsync)}");
IsRunning = true;
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation($"{nameof(CustomBackgroundService)} running {nameof(ExecuteAsync)}");
await Task.Delay(5000);
}
IsRunning = false;
_logger.LogInformation($"{nameof(CustomBackgroundService)} ending {nameof(ExecuteAsync)}");
}
catch (Exception exception)
{
_logger.LogError(exception.Message, exception);
}
finally
{
IsRunning = false;
}
}
}

Registering and adding the hosted service:

services
.AddLogging(logging => logging.AddConsole())
.AddSingleton<WeatherForecastService>()
.AddSingleton<CustomBackgroundService>()
.AddHostedService(serviceCollection => serviceCollection.GetRequiredService<CustomBackgroundService>());

I added a new Razor page and added it to the menu (Pages/Service.razor):

@page "/Service"
@inject CustomBackgroundService _customBackgroundService
<h3>Service</h3><p><div hidden="@HideIsRunning">Running</div></p><button name="startButton" class="btn btn-primary" @onclick="Start">Start</button>
<button class="btn btn-primary" @onclick="Stop">Stop</button>
@code { private bool _isRunning { get; set; }
public bool HideIsRunning => !_isRunning;

protected override void OnInitialized()
{
_isRunning = _customBackgroundService.IsRunning;
base.OnInitialized();
}
private async Task Start()
{
if(!_customBackgroundService.IsRunning)
await _customBackgroundService.StartAsync(new System.Threading.CancellationToken());
_isRunning = _customBackgroundService.IsRunning;
}

private async Task Stop()
{
if(_customBackgroundService.IsRunning)
await _customBackgroundService.StopAsync(new System.Threading.CancellationToken());
_isRunning = _customBackgroundService.IsRunning;
}
}

Adding a new menu item to the default Blazor application by changing `Shared/NavMenu.razor’:

<div class="nav-item px-3">
<NavLink class="nav-link" href="Service">
<span class="oi oi-pulse" aria-hidden="true"></span> Service
</NavLink>
</div>

When debugging the project this should be visible:

you can start and stop the service and check the console of your IDE to see the output:

Running and installing the service

To run the application as a Service, the following code has to be added to the Program.cs:

public static void Main(string[] args)
{
var isService = !(Debugger.IsAttached || args.Contains("--console"));
if (isService)
Directory.SetCurrentDirectory(Environment.ProcessPath!);
var builder = CreateHostBuilder(args.Where(arg => arg != "--console").ToArray()); if (isService)
{
if (OperatingSystem.IsWindows())
builder.UseWindowsService();
else if (OperatingSystem.IsLinux())
builder.UseSystemd();
else
throw new InvalidOperationException(
$"Can not run this application as a service on this Operating System");
}
builder.Build().Run();
}

Next, install the following NuGet packages:

.NET Service on Windows

First, publish the application to a folder. Make sure to create the correct publish profile for Windows. Also, consider if you need to publish it Framework-Dependent (the .NET framework has to be installed on the host machine) or Self-Contained (everything needed is included, how cool is that!):

After publishing, open a Powershell command line, go to the directory containing the newly published service and execute the following commands:

  • New-Service -Name “Blazor Background Service” -BinaryPath .\BlazorBackgroundservice.BlazorUI.exe
  • Start-Service -Name “BlazorBackgroundService”

I could write logs to the Event Logger, but I decided to write simple logs to a text file. When you look into the directory of the service, you should see a logfile log****.txt. Look into the log file to see if the service is running. When going to the url’s provided in the log file, be aware that the HTTPS port might not work because there are no valid SSL certificates installed.

.NET Service on Linux

Same as for Windows: publish the application to a folder, using the correct publishing configuration. I can test the application by adding --console to the command line:

To install it as a service, I created the file /etc/systemd/system/blazorbackgroundservice.service:

[Unit]
Description=Blazor Background Service
[Service]
Type=Notify
ExecStart=/home/jacob/blazorbackgroundservice/linux/BlazorBackgroundService.BlazorUI
[Install]
WantedBy=multi-user.target

Run the following commands:

  • sudo systemctl daemon-reload
  • sudo systemctl status blazorbackgroundservice
  • sudo systemctl start blazorbackgroundservice
  • sudo systemctl status blazorbackgroundservice

It works! Check the status output for the URL of the Blazor website and browse to the site to check if it works.

You can even auto-start the service by running the following command:

  • sudo systemctl enable blazorbackgroundservice

Next time you reboot, the service will automatically start.

Conclusion

Although this is a short post with nothing fancy, I myself can’t wait to implement some services this way. I am still thinking of some use-cases, but I can imagine a service that gathers data, processes it and sends it to a service online. Any thoughts on other use-cases? Please let me know!

Sources

--

--

Jacob Duijzer
Team Rockstars IT

Senior Software Engineer and Team Lead. Passionate about Software Engineering, Quality, Testing and Training developers.