3

I can find tutorials to add it to MVC, and even from 2014, an article explaining how to add it to .NET 4.7 windows service.

However with a .NET Core 3.1 windows service, I just cannot figure out how to do it.

Most tutorials seem to revolve around a Startup.cs file which does not exist in a windows service. This is the latest tutorial I could find from Microsoft but it uses a Web App rather than a windows service.

The windows service runs using this code:

var builder = new HostBuilder() .ConfigureServices((hostContext, services) => { services.AddHostedService<MyWindowsService>(); });

I assume that SignalR needs to be set up around here.

I found some evidence you can do WebApp.Start("http://localhost:8080"); but this is OWIN. The example I found (an older version of the one above) then has a Startup class but there's no indication how this class is called. It takes an IAppBuilder and there's a method on it to add SignalR. However IAppBuilder does not appear to be .NET Core, nor could I find any SignalR methods of any kind.

I wonder if anyone could point me in the right direction?

NibblyPig
  • 51,118
  • 72
  • 200
  • 356

2 Answers2

5

For people wanting to develop a self-hosted ASPNETCore SignalR hub running in a Windows service, here's my barebones code. (Disclaimer: I'm new to ASPNET Core, and I don't know whether this approach would be approved by more knowledgeable folk.) The magic is in the ".UseStartup();" call.

  1. Create a new service project using the VS 2019 "Worker Service" C# template.

  2. Edit the service's .csproj file and insert the lines:

        <ItemGroup>
            <FrameworkReference Include="Microsoft.aspNetCore.App" />
        </ItemGroup>
    
  3. Create a Startup class:

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace My.SignalRCore.Service
    {
        public class Startup
        {
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSignalR();
                services.AddHostedService<Worker>();
            }
    
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                app.UseRouting();               // pre-requisite for app.UseEndpoints()
                app.UseEndpoints(endpoints =>
                {
                    string url = $"/ServerHub";
                    endpoints.MapHub<MyHub>(url);
                });
            }
        }
    }
    
  4. Create a MyHub : Hub class:

    using Microsoft.AspNetCore.SignalR;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace My.SignalRCore.Service
    {
        public class MyHub : Hub
        {
            public ILogger<Worker> _logger = null;
    
            public MyHub(ILogger<Worker> logger)
            {
                _logger = logger;
                //_logger.LogInformation($"{DateTimeOffset.Now} MyHub.Constructor()");
    
            }
            public async Task ProcessClientMessage(string user, string message)
            {
                // process an incoming message from a connected client
                _logger.LogInformation($"{DateTime.Now.ToString("hh:mm:ss.fff")}  MyHub.ProcessClientMessage({user}, {message})");
    
            }
        }
    }
    
  5. Amend the Program class to use a "UseStartup" call:

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Hosting;
    using System;
    
    namespace My.SignalRCore.Service
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.UseUrls("http://*:12457");
                });
        }
    }
    
  6. Add a hub reference (if needed) in the Worker class:

    using Microsoft.AspNetCore.SignalR;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace My.SignalRCore.Service
    {
        public class Worker : BackgroundService
        {
            private readonly ILogger<Worker> _logger;
            private readonly IHubContext<MyHub> _signalRHub;
    
            public Worker(ILogger<Worker> logger, IHubContext<MyHub> signalRHub)
            {
                _logger = logger;
                _signalRHub = signalRHub;
            }
    
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    await Task.Delay(15000, stoppingToken);
                    _logger.LogInformation($"{DateTime.Now.ToString("hh:mm:ss.fff")} Sending ping to all clients");
                    await _signalRHub.Clients.All.SendAsync("ReceiveMessage", "Server", "ping");
                }
            }
        }
    }
    

That's it for the server code. I've not yet installed it as a service, but it works as a console app. On a non-dev machine, you might need to install the APSNET CORE 3.1 runtime, it's available here: https://dotnet.microsoft.com/download/dotnet/3.1

For the client:

  1. Install nuget package: Microsoft.AspNetCore.SignalR.Client

  2. Create a client class along the lines of (note: the reconnect code here isn't working):

    using Microsoft.AspNetCore.SignalR.Client;
    using System;
    using System.Threading.Tasks;
    
    namespace My.SignalRCoreClientLib
    {
        public class SignalRCoreClientLib
        {
            public EventHandler<string> MessageEvent;
    
            private HubConnection _connection;
    
            public async Task Connect(string serverIp, int port)
            {
                if (_connection == null)
                {
                    _connection = new HubConnectionBuilder()
                        .WithUrl($"http://{serverIp}:{port}/ServerHub")
                        .Build();
    
                    _connection.Closed += async (error) =>
                    {
                        await Task.Delay(new Random().Next(0, 5) * 1000);
                        await _connection.StartAsync();
                    };
    
                    _connection.On<string, string>("ReceiveMessage", (user, message) =>
                    {
                        string fullMessage = $"{user}: {message}";
                        MessageEvent?.Invoke(this, fullMessage);
                    });
                }
                try
                {
                    await _connection.StartAsync();
                }
                catch (Exception ex)
                {
                    MessageEvent?.Invoke(this, $"{ex.Message}; base Exception: {ex.GetBaseException().Message}");
                    await Task.Delay(new Random().Next(0, 5) * 1000);
                    await Connect(serverIp, port);
                }
            }
    
            public async Task SendMessage(string user, string message)
            {
                try
                {
                    await _connection.InvokeAsync("ProcessClientMessage", user, message);
                }
                catch (Exception ex)
                {
                    MessageEvent?.Invoke(this, ex.Message);
                }
            }
        }
    }
    

That's it. Hope this is helpful.

TimA_12345
  • 51
  • 1
  • 2
2

SignalR Server-side requires a server that receives web requests, Kestrel or IIS normally. So you need a web app, you can still add hosted services to your webapp, there is even an example showing a web app with SignalR and a hosted service: https://learn.microsoft.com/aspnet/core/signalr/background-services?view=aspnetcore-5.0

Brennan
  • 1,834
  • 1
  • 7
  • 13
  • It says you can self-host, and there are examples for doing it in a windows service, however they don't stretch to .net core – NibblyPig Nov 15 '20 at 00:23
  • 2
    ASP.NET Core is self-host by default. The doc about windows service mentions running a web app as a service https://learn.microsoft.com/aspnet/core/host-and-deploy/windows-service?view=aspnetcore-5.0&tabs=visual-studio#sdk – Brennan Nov 15 '20 at 03:40
  • I don't really understand, perhaps it is impossible to do it with .NET. I'll just create a website instead and figure out a way to forward messages to a windows service. Thanks though. – NibblyPig Nov 15 '20 at 11:55