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

My employee has developed an application which “scrapes” data from systems, processes it and sends it to a central database. This is a WinForms application with a few screens for configurations and inspections. I was looking into different approaches for a new version 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 install Blazor as a UI for a Windows Service. I could configure, start and stop, basically do all kind of things with this service if I have a Blazor 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 just because I can).

Well, after a few hours, I got it working so, in this post I will show you how to create a cross-platform service that can be installed on Windows (Service) and Linux (systemd) 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, AFAIK). 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:

Blazor image

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

Blazor image

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!):

Windows publish

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

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

Stopped on Windows

  • Start-Service -Name “BlazorBackgroundService”

Stared on Windows

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 logfile to see if the service is running. When going to the url’s provided in the logfile be aware that the https port might not work because there are no valid SSL certificates installed.

Logging on Windows

.NET Service on Linux

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

Running on Arch Linux

To install it as a service I created the file `` in /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

Stopped service

  • sudo systemctl start blazorbackgroundservice
  • sudo systemctl status blazorbackgroundservice

Running service

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 imaging 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