Search code examples
c#asp.net-coredependency-injectionasp.net-core-signalr

ASP.NET Core SignalR acces Hub method from anywhere


If been spending many hours on this problem and I found a lot of different strategies, but none of them worked for me. (This code is just a proof of concept ofcourse.)

I have the following setup using Asp.net core 2.1 (on .Net Framwork 4.7.2):

I have made a signalr hub which has a method to send a number:

using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;


namespace TestRandomNumberSignalR
{
    public class TestHub : Hub
    {
        public async Task SendRandomNumber(int number)
        {
            await Clients.All.SendAsync("ReceiveRandomBumber", number);
        }
    }
}

I've also made a class that updates a random number every 3 seconds and added it as a singleton:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace TestRandomNumberSignalR
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton(new UpdateRandomNumber());
            services.AddSignalR();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();
            app.UseSignalR(routes =>
            {
                routes.MapHub<TestHub>("/testHub");
            });
        }
    }
}

Here is the random number class:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TestRandomNumberSignalR
{
    public class UpdateRandomNumber
    {
        private bool _continue = true;

        public UpdateRandomNumber()
        {
            var task = new Task(() => RandomNumberLoop(),
                                TaskCreationOptions.LongRunning);
            task.Start();
        }

        private void RandomNumberLoop()
        {
            Random r = new Random();

            while (_continue)
            {
                Thread.Sleep(3000);
                int number = r.Next(0, 100);
                Console.WriteLine("The random number is now " + number);

                // Send new random number to connected subscribers here
                // Something like TestHub.SendRandomNumber(number);

            }
        }

        public void Stop()
        {
            _continue = false;
        }
    }
}

Now from this class (as I wrote in the comment) I want to send the new random number using SignalR. Only how to get the hub context in there?

Also I want to be able to acces the Stop() method on the class from within a controller, how can I acces that?

I now this is a well discussed subject, but still I can't find a working solution anywhere. Hope you can help me.

EDIT

Question 1

While the random loop is starting now (with many thanks to rasharasha), still some questions remain. I'm now unable to inject the proper UpdateRandomNumber into a controller. Lets say I want to be able to stop the loop calling the UpdateRandomNumber.Stop() method, how can I inject the UpdateRandomNumber singleton into a controller. I tried creating an interface:

public interface IUpdateRandomNumber
{
    void Stop();
}

Changing the RandomNumber method to implement this:

public class UpdateRandomNumber : IUpdateRandomNumber
{
    private bool _continue = true;

    private IHubContext<TestHub> testHub;

    public UpdateRandomNumber(IHubContext<TestHub> testHub)        
    {
        this.testHub = testHub;

        var task = new Task(() => RandomNumberLoop(),
                            TaskCreationOptions.LongRunning);
        task.Start();
    }

    private void RandomNumberLoop()
    {
        Random r = new Random();

        while (_continue)
        {

            Thread.Sleep(3000);
            int number = r.Next(0, 100);
            Console.WriteLine("The random number is now " + number);

            // Send new random number to connected subscribers here
            // Something like TestHub.SendRandomNumber(number);

        }
    }

    public void Stop()
    {
        _continue = false;
    }
}

And changing the add singleton method so it wil use the interface:

        services.AddSingleton<IUpdateRandomNumber>(provider =>
        {
            var hubContext = provider.GetService<IHubContext<TestHub>>();
            var updateRandomNumber = new UpdateRandomNumber(hubContext);
            return updateRandomNumber;
        });

I can now create a controller with a method to stop the randomnumber loop:

[Route("api/[controller]")]
[ApiController]
public class RandomController : ControllerBase
{
    private readonly IUpdateRandomNumber _updateRandomNumber;

    public RandomController(IUpdateRandomNumber updateRandomNumber)
    {
        _updateRandomNumber = updateRandomNumber;
    }

    // POST api/random
    [HttpPost]
    public void Post()
    {
        _updateRandomNumber.Stop();
    }

However, this implementation will prevent the loop from starting again. So how can I acces the rondomnumber singleton from a controller?

Question 2

From my UpdateRandomNumber class I can now call:

testHub.Clients.All.SendAsync("ReceiveRandomBumber", number);

But why did I make the method in my testhub:

    public async Task SendRandomNumber(int number)
    {
        await Clients.All.SendAsync("ReceiveRandomBumber", number);
    }

It would be much more convienent to create the methods in the hub and them call them directly. Can this be done?


Solution

  • You can inject the TestHub into the controller using Constructor Injection. Since its already registered in the DI Container.

    public class UpdateRandomNumber
    {
        private bool _continue = true;
        private IHubContext<TestHub> testHub;
        private Task randomNumberTask;
        public UpdateRandomNumber(IHubContext<TestHub> testHub)
        {
            this.testHub=testHub;
            randomNumberTask = new Task(() => RandomNumberLoop(),
                TaskCreationOptions.LongRunning);
            randomNumberTask.Start();
        }
        private async void RandomNumberLoop()
        {
            Random r = new Random();
    
            while (_continue)
            {
                Thread.Sleep(3000);
                int number = r.Next(0, 100);
                Console.WriteLine("The random number is now " + number);
    
                // Send new random number to connected subscribers here
                 await testHub.Clients.All.SendAsync($"ReceiveRandomNumber", number);
    
            }
        }
    
        public void Stop()
        {
            _continue = false;
        }
    }
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
    
            services.AddSignalR();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddSingleton(provider =>
            {
                var hubContext = provider.GetService<IHubContext<TestHub>>();
                var updateRandomNumber = new UpdateRandomNumber(hubContext);
                return updateRandomNumber;
            });
    
        }
    
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var updateRandonNumber = app.ApplicationServices.GetService<UpdateRandomNumber>();
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
    
            app.UseMvc();
            app.UseSignalR(routes =>
            {
                routes.MapHub<TestHub>("/testHub");
            });
        }
    }