1

From a Page I'm calling a service like so:

_ = _importUserService.AddManyUserAsync(ValidatedUsers);

I want the Page() to return and let the user/UI move on while the service adds many users utilizing UserManager in the background. The problem is that as soon as the Page() returns and the request is finished, the IdentityDbContext is closed and UserManager no longer functions. How do I keep UserManager working after a request is finished?

The service is using standard dependency injection:

    private readonly UserManager<ApplicationUser> _userManager;

    public ImportUserService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

It works fine if I use await instead of the discard and force the service to finish before the page is loaded; however, this is terrible UX if the user needs to wait 2 minutes for thousands of users to be imported before the page loads.. My question is without creating a stored procedure and replicating what UserManager is doing, is there a way to use UserManager in a service that is not dependent on the lifetime of a request?

My thoughts are to create a new instance of UserManager and not rely on the instance from the dependancy injection, but I have so far been unable to get that to work. Not sure what the syntax would be and cannot find an example that actually works in .net core 3.1

How do I keep IdentityDbContext alive after a request so that a service can complete its task using UserManager? (.Net Core 3.1)

johnmweisz
  • 81
  • 1
  • 4
  • Why don't you want to `await` the operation? You'd like to know if adding the users actually succeeded or failed, wouldn't you? – Pieterjan Mar 02 '21 at 20:58
  • However the following SO answer has a good implementation: https://stackoverflow.com/a/49814520/8941307 – Pieterjan Mar 02 '21 at 21:07
  • If I await the operation, it no longer functions asynchronously as required. – johnmweisz Mar 02 '21 at 21:26
  • The internal code of `AddUsersAsync` is in fact (perhaps, probably but not necessarily, in this case it IS) running on another thread. Just because you `await` the outer Task, doesn't all of a sudden make your operation synchronous. – Pieterjan Mar 02 '21 at 21:50
  • await is not the issue - I guess the specific question I'm asking is; how do I keep IdentityDbContext alive after a request so that a service can complete its task using UserManager – johnmweisz Mar 02 '21 at 22:29
  • 2
    From a request you can't. A `DbContext` is always registered as scoped service. So when your webrequest is finished, your `DbContext` is finished. You could do so from a `HostedService` (see the second comment). – Pieterjan Mar 03 '21 at 06:41
  • 2
    Please read [Stephen Cleary](https://stackoverflow.com/users/263693/stephen-cleary)'s [Asynchronous Messaging](https://blog.stephencleary.com/2021/01/asynchronous-messaging-1-basic-distributed-architecture.html) blog series to get better understanding how you should handle this kind of sitatuion. – Peter Csala Mar 03 '21 at 09:21
  • @Pieterjan, ok makes sense - thank you! – johnmweisz Mar 03 '21 at 18:43
  • @PeterCsala very interesting, thanks for the info. – johnmweisz Mar 03 '21 at 18:44
  • I think you should also consider improving the UX aspect of this operation purely at the frontend. Because doing it via XHR/Ajax, adding a spinner and allowing the user to do other stuff in the meanwhile might be way easier than implementing a robust background service in the backend. Of course this totally depends on your frontend/backend expertise; plus obviously on the general business requirements and the nature of the site. I just thought it might worth mentioning. – Leaky Mar 06 '21 at 18:16
  • @Leaky this is kind of how the existing platform does it and is the reason why I'm rebuilding it; users often leave the page or close the browser before the task is complete, and processing 10,000 users takes about 8 minutes; therefore, a background task is certainly the best solution in this case. A user can now add thousands of users under less than a second, because validation is extremely fast, but actually adding the users is not. thanks for the reply though! – johnmweisz Mar 15 '21 at 02:41

1 Answers1

0

From the documentation

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#consuming-a-scoped-service-in-a-background-task

This allowed me to run a background service that can call a service that uses UserManager

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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>();
            })
            .ConfigureServices(services =>
            {
                services.AddHostedService<ConsumeScopedServiceHostedService>();
            });
}
public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

and the work is done in IScopedProcessingService > DoWork(stoppingToken) meaning ScopedProcessingService has access to the same services that a typical web service would have using the dependency injection shown in my question.

johnmweisz
  • 81
  • 1
  • 4