Search code examples
c#.net-coreentity-framework-coreasp.net-core-hosted-services

Entity framework with Scoped Hosted Services


I have the following interface

internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

and implementation

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 Task.CompletedTask;
    }
}

This code is from official Microsoft documentation. Background scoped services

And like in the documentation I have ScopedProcessingService but a few more difficult. Here is the code:

internal class ScopedProcessingService : IScopedProcessingService
{
    private int _executionCount;

    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    private readonly IPushRepository _pushRepository;
    private readonly IPushTemplateRepository _pushTemplateRepository;
    private readonly ISenderLogRepository _senderLogRepository;
    private readonly IDistributionRepository _distributionRepository;

    // services
    private readonly IPushTemplateService _pushTemplateService;
    private readonly ISendPushService _sendPushService;


    public ScopedProcessingService(
        ILogger<ConsumeScopedServiceHostedService> logger,
        IPushTemplateService pushTemplateService, ISendPushService sendPushService,
        IPushRepository pushRepository,
        ISenderLogRepository senderLogRepository, IDistributionRepository distributionRepository,
        IPushTemplateRepository pushTemplateRepository)
    {
        _logger = logger;
        _pushTemplateService = pushTemplateService;
        _sendPushService = sendPushService;
        _pushRepository = pushRepository;
        _senderLogRepository = senderLogRepository;
        _distributionRepository = distributionRepository;
        _pushTemplateRepository = pushTemplateRepository;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _executionCount = _senderLogRepository.SenderLogs.Count();

            var logMessage = new StringBuilder();

            logMessage.AppendLine($"Начинаю рассылку № {_executionCount}.");

            // get all templates. THIS CALL IS A SOURCE OF PROBLEMS
            var templates = _pushTemplateRepository.PushTemplates.Where(x => x.isActive)
                .Include(x => x.Messages)
                .ThenInclude(x => x.PushLang)
                .Include(x => x.Category)
                .Include(x => x.AdvertiserPushTemplates)
                .ThenInclude(x => x.Advertiser)
                .ToList();
    }
}

In the Startup.cs class I use the following code to inject it:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

The problem with this line var templates = _pushTemplateRepository.PushTemplates.Where(x => x.isActive). If I make some changes with a PushTemplate this changes will not have effect in the background task. And I will process old data. I mean, If I change name for a PushTemplate, for example, with id = 15 from Name_1 to Name_2 than I will have Name_1 in the background task.

How to inject EF in a Scoped background service correctly? I not use clear EF context. I have repository layer.

public interface IPushTemplateRepository
{
    IQueryable<PushTemplate> PushTemplates { get; }

    void Save(PushTemplate pushTemplate);
    void Delete(int templateid);
}

And implementation

public class PushTemplateRepository : IPushTemplateRepository
{
    private readonly ApplicationDbContext _applicationContext;

    public PushTemplateRepository(ApplicationDbContext applicationContext)
    {
        _applicationContext = applicationContext;
    }

    public IQueryable<PushTemplate> PushTemplates => _applicationContext.PushTemplates;

    public void Save(PushTemplate pushTemplate)
    {
      // ...
    }

    public void Delete(int templateid)
    {
      // ... 
    }
}

Solution

  • The issue is the captured DbContext in that single scope that has an infinite loop.

    The scope is never disposed so will retain the data it had when the scope was created.

    Refactor to move the loop out a level and create a new scope each time the desired functionality is required.

    ConsumeScopedServiceHostedService

    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        _logger.LogInformation("Consume Scoped Service Hosted Service is working.");
    
        while (!stoppingToken.IsCancellationRequested) {
            using (var scope = Services.CreateScope()) {
                IServiceProvider serviceProvider = scope.ServiceProvider;
                var service = serviceProvider.GetRequiredService<IScopedProcessingService>();    
                await service.DoWork(stoppingToken);
            }
            //Add a delay between executions.
            await Task.Delay(SomeIntervalBetweenCalls, stoppingToken);
        }
    }
    

    ScopedProcessingService

    //...
    
    public async Task DoWork(CancellationToken stoppingToken) {
        _executionCount = _senderLogRepository.SenderLogs.Count();
    
        var logMessage = new StringBuilder();
    
        logMessage.AppendLine($"Начинаю рассылку № {_executionCount}.");
    
        // get all templates.
        var templates = _pushTemplateRepository.PushTemplates.Where(x => x.isActive)
            .Include(x => x.Messages)
            .ThenInclude(x => x.PushLang)
            .Include(x => x.Category)
            .Include(x => x.AdvertiserPushTemplates)
            .ThenInclude(x => x.Advertiser)
            .ToList();
    
        //...
    }