Search code examples
c#asp.net-core-mvcwpf-core

.Net Core 6 - MVC, SignalR and HostedService to be called from WPF app


I have an .NET Core 6 MVC app, that contains a SignalR Hub and a HostedService implementing IHostedService. I have also a WPF app, which when the user clicks a button, should call the server and do some long task on the HostedService, then the WPF has to receive updates about the progress to display them. Dependency Injection is implemented in the MVC app, but as of now I am not using it for the HostedService.

I have it "somehow" implemented it, but I think in a wrong way. I will try to explain how I am doing it right now: Basically, when user clicks a button on the WPF app, it sends an httprequest to an actionmethod on a MVC controller, here it creates an instance of the HostedService and calls its StartAsync() method. As I don't await this in the actionmethod, once the job is started on the HostedService the action method will continue running and finish the http request. Then the WPF will keep being updating from the calls that the HostedService does through the SignalR Hub.

The HostedService has some dependencies, specifically: an EF db context, the SignalR Hub, and also some other of my custom services. right now, as I am not using DI for the hosted service, I am creating the dbcontext inside the HostedService, the Hub is received from the controller when I create the hostedService, and the other dependencies are being created manually inside the HostedService...

Ok, this is at least working ok. BUT:

First, I have the feeling that I'm not doing this "the right way".. Second, I want to use DI to inject the HostedServices dependencies (the EF db context, the SignalR Hub, and the other services). Third, I want to refactor everything so there is no need for the controller and actionmethod (I mean, do it so that the WPF directly calls the SignalR Hub to start the process, instead of making an http request to MVC controller)

I have been researching about this, but the more I read about it, the more confused I am. For example, it seems the hostedservice should be started when MVC app starts, and then its StartAsync() method will be called automatically. But I don't want the long process to run when MVC is started, I want it to run when it receives a call from WPF client app. All the tutorials/examples I see, are about the hosted service doing some periodic task (like sending something to the client every 5 seconds), but this is not what I need. So I have no idea how I should do this.

(Note - the SignalR Hub is also being used for some other tasks besides this)

The code I have right now is something like this:

The controller and action method:

public class SincroController : Controller {
        private readonly ApplicationDbContext _db;
        private readonly IConfiguration _config;
        private readonly IHubContext<SincroHub> _hubContext;
        private readonly IMyServiceA _myServiceA;
    
        //ctor
        public SincroController(ApplicationDbContext context, IConfiguration config, IHubContext<SincroHub> hubContext, IMyServiceA myServiceA) {
            _db = context;
            _config = config;
            _hubContext = hubContext;
            _myServiceA = myServiceA;
        }
    
        //the action method
        public async Task<JsonResult> DoSincro(string pathFolderSincro) {
            //create instance of hostedservice and starts it
            SincroHostedService sincroHostedService = new SincroHostedService(pathFolderSincro, _hubContext, _config, _myServiceA);
            sincroHostedService.StartAsync(new CancellationToken());

            var json = new { msg = "task started" };
            return Json(json);
        }
}

The hosted service:

public class SincroHostedService : IHostedService {
    static string connstring = "Server=(localdb)\\..........";
    private readonly ApplicationDbContext _db;
    private readonly IConfiguration _config;
    private readonly IHubContext<SincroHub> _hubContext;
    private readonly IMyServiceA _myServiceA;
    private string _someNeededPath;
   


    //ctor
    public SincroHostedService(string path, IHubContext<SincroHub> hubContext, IConfiguration config, IMyServiceA myServiceA) {
        //instances dbcontext
        var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlServer(connstring).Options;
        _db = new ApplicationDbContext(dbContextOptions);

        _hubContext = (IHubContext<SincroHub>)hubContext;
        _config = config;
        _myServiceA = myServiceA;
        _someNeededPath = path;
    }


    public Task StartAsync(CancellationToken cancellationToken) {
        Task.Run(async () => {
    processEverything();
        });
        return Task.CompletedTask;
    }


    private async Task<int> processEverything() {
        //this does all the work, using the dbcontext, the hub (to send udpates to the WPF app), and my custom services...
        for(int i=0; i<10000; i++) {
            //does its things...
            //...
            //send notification from hub to WPF client app
            await _hubContext.Clients.All.SendAsync("NotifySomethingFromHub", i);
            await Task.Delay(2);
        }
    }

}

One of my custom services:

public interface IMyServiceA {
    public void someMethod();
}

public class MyServiceA : IMyServiceA {
    private readonly ApplicationDbContext _db;
    private readonly IMyServiceB _myServiceB;

    //ctor
    public MyServiceA(ApplicationDbContext dbcontext, IMyServiceB myServiceB) {
        _db = dbcontext;
        _myServiceB = myServiceB;
    }

    public void someMethod(){
        //...
    }
}

In program.cs of MVC app:

public class Program {
    public static void Main(string[] args) {
        var builder = WebApplication.CreateBuilder(args);

        var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
        builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

        builder.Services.AddSignalR();
        builder.Services.AddTransient<IMyServiceA, MyServiceA>();
        builder.Services.AddTransient<IMyServiceB, MyServiceB>();

        var app = builder.Build();

        //...

        app.MapHub<SincroHub>("/SincroHub");

        app.Run();
    }
}

In WPF app:

private async void btnProcesar_Click(object sender, RoutedEventArgs e) {
    bool result = await processSincro();
}

private async Task<bool> processSincro() {
    //connecs to signalR hub 
    HubConnection connection = new HubConnectionBuilder().WithUrl("...url of hub endpoint...").Build();
    await connection.StartAsync();

    //declares signalR hub handles
    connection.On<int>("NotifySomethingFromHub", someData) => {
        //updates ui
        //...
    });

    return true;
}

The SignalR Hub:

public class SincroHub : Hub {
    private readonly ApplicationDbContext _db;  //dbcontext q se recibe inyectado en el constructor de este hub
    private readonly IDownloadsService _downloadsService;

    //ctor
    public SincroHub(ApplicationDbContext dbcontext, IDownloadsService downloadsService) {
        _db = dbcontext;
        _downloadsService = downloadsService;
    }
}

Solution

  • I would probably implement it this way

    1. In WPF app on button click I would call server from client via SignalR
    2. For the long-running job I would use HostedService, but it should not run the long-run job by default
    3. For transferring an event from Hub to HostedService I would use Observable - Hub will publish an event, while HostedService subscribes once during creation to this event, listen and react to it. My personal preference is Rx, but you can check another sample here How to run a background service on demand - not on application startup or on a timer