Как использовать сервис с областью действия с зависимостью в фоновой задаче

Вот мой сценарий. Я хочу использовать фоновую задачу для рассылки информационных бюллетеней подписанным пользователям. Это выполняется MailService, у которого в качестве зависимости есть UnitOfWork.

Я попробовал решение из docs. microsoft.com, поэтому в моем случае я использую метод IMailService вместо ILogger, но получаю сообщение об ошибке:

System.InvalidOperationException: «Невозможно использовать службу с заданной областью действия>« Fit4You.Core.Data.IUnitOfWork »из синглтона>« Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor ».

Я не хочу, чтобы у моего UnitOfWork или DbContext было время жизни Singleton. Можно ли каким-то образом использовать зависимость MailService с областью действия UnitOfWork?

Я знаю об IServiceScopeFactory, но, возможно, не знаю, как его правильно использовать.

Я использую .NET Core 2.2 и встраиваю интерфейс 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
        }
    }

comment
Попробуйте следующее: stackoverflow.com/questions/42984052/. Дело в том, что DI чувствителен к времени жизни объекта. Вы можете потреблять то, что имеет такое же или большее время жизни, потому что таким образом вы уверены, что используемый вами объект не будет удален в середине процесса.   -  person Harun Ćerim    schedule 29.03.2019
comment
В документе, на который вы ссылаетесь, уже показано, как это сделать. Оно работает. Ошибка указывает на то, что размещенная служба все еще зависит от IUnitOfWork. Вы не опубликовали этот код, но я подозреваю, что в нем все еще есть параметр конструктора IUnitOfWork.   -  person Panagiotis Kanavos    schedule 29.03.2019
comment
@Panagiotis Kanavos, скажите, пожалуйста, какой код вам нужен для определения решения   -  person Sadinus    schedule 29.03.2019
comment
Вам следует удалить параметр IMailService mailService из конструктора ConsumeScopedMailService. MailService косвенно зависит от службы UnitOfWork с заданной областью действия. В любом случае ваш код никогда не использует значение mailService.   -  person Panagiotis Kanavos    schedule 29.03.2019
comment
@SteveLand совсем нет. OP уже использовал этот код, даже ссылки на документы. Вопрос в том, почему все еще возникает InvalidOperationException. Ответ заключается в том, что есть косвенная ссылка на сервис с ограниченным объемом   -  person Panagiotis Kanavos    schedule 29.03.2019
comment
@PanagiotisKanavos - ты прав, отказался.   -  person Steve Land    schedule 29.03.2019


Ответы (1)


Синглтон ConsumeScopedMailService зависит от IMailService через свой конструктор:

public ConsumeScopedMailService(IServiceProvider services, IMailService mailService)

IMailService может быть временным, но класс, который его реализует, зависит от службы с областью действия, IUnitOfWork. Косвенно ConsumeScopedMailService попадает в зависимость от службы с заданной областью действия.

Чтобы исправить это, IMailService mailService следует удалить. В любом случае он не используется в опубликованном коде.

person Panagiotis Kanavos    schedule 29.03.2019