How to consume scoped service with dependency in a background task
H

1

8

Here is my scenerio. I want to use background task to send newsletter to subscribed users. This is done by MailService, which has UnitOfWork as dependency.

I tried the solution from learn.microsoft.com so in my case I use a method of IMailService instead of ILogger, but I'm getting an error:

System.InvalidOperationException: 'Cannot consume scoped service >'Fit4You.Core.Data.IUnitOfWork' from singleton >'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'.'

I don't want to make my UnitOfWork or DbContext with Singleton lifetime. Is it possible to consume somehow a dependency of MailService which is scoped UnitOfWork?

I know about IServiceScopeFactory but maybe don't know how to use it properly.

I'm using .NET Core 2.2 and build in interface IHostedService

ScopedMailService:

public class ScopedMailService : IScopedMailService
{
    private readonly IMailService mailService;

    public ScopedMailService(IMailService mailService)
    {
        this.mailService = mailService;
    }

    public void DoWork()
    {
        mailService.SendNewsletterToSubscribedUsers();
    }
}

ConsumeScopedMailService:

public class ConsumeScopedMailService : IHostedService
{
    private Timer timer;
    private readonly IMailService mailService;
    public IServiceProvider Services { get; }

    public ConsumeScopedMailService(IServiceProvider services, IMailService mailService)
    {
        Services = services;
        this.mailService = mailService;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        var startTimeSpan = GetStartTimeSpan();
        var periodTimeSpan = TimeSpan.FromSeconds(30);

        timer = new Timer(DoWork, null, startTimeSpan, periodTimeSpan);

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        using (var scope = Services.CreateScope())
        {
            var scopedMailService = scope.ServiceProvider.GetRequiredService<IScopedMailService>();
            scopedMailService.DoWork();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        timer?.Dispose();
    }

    private TimeSpan GetStartTimeSpan()
    {
        var currentTime = DateTime.Now.Ticks;
        var executeTime = DateTime.Today.AddHours(8)
                                        .AddMinutes(0)
                                        .Ticks;

        long ticks = executeTime - currentTime;

        if (ticks < 0)
        {
            ticks = ticks + TimeSpan.TicksPerDay;
        }

        var startTimeSpan = new TimeSpan(ticks);

        return startTimeSpan;
    }
}

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<Fit4YouDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString")));

    services.AddScoped<IUnitOfWork, UnitOfWork>();
    services.AddTransient<IMailService, MailService>();

    services.AddHostedService<ConsumeScopedMailService>();
    services.AddScoped<IScopedMailService, ScopedMailService>();

    ...
}

MailService:

    public class MailService : IMailService
    {
        private readonly IUnitOfWork unitOfWork;
        public MailService(IUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
        }

        public void SendNewsletterToSubscribedUsers()
        {
            // Some Code
        }
    }
Hoosegow answered 29/3, 2019 at 8:53 Comment(6)
Try this: #42984552. The thing is that DI is sensitive to the lifetime of the object. You can consume something that has the same or a bigger lifetime because by that way you are certain that the object you are using won't get disposed in the middle of the process.Subtropics
The document you link to already shows how to do this. It works. The error complains that the hosted service still has a dependency on IUnitOfWork. You haven't posted that code but I suspect it still has an IUnitOfWork costructor parameterGomez
@Panagiotis Kanavos please tell me which code do you need to define the solutionHoosegow
You should remove the IMailService mailService parameter from ConsumeScopedMailService's constructor. MailService indirectly depends on the scoped UnitOfWork service. Your code never uses the mailService value anyway.Gomez
@SteveLand not at all. The OP already used that code, even links to the docs. The question is why InvalidOperationException still occurs. The answer is that there's an indirect reference to a scoped serviceGomez
@PanagiotisKanavos - you're right, retracted.Keenakeenan
G
4

The singleton ConsumeScopedMailService depends on IMailService through its constructor :

public ConsumeScopedMailService(IServiceProvider services, IMailService mailService)

IMailService may be transient but the class that implements it depends on a scoped service, IUnitOfWork. Indirectly, ConsumeScopedMailService ends up depending on a scoped service.

To fix this IMailService mailService should be removed. It's not used in the posted code anyway.

Gomez answered 29/3, 2019 at 9:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.