Как правильно добавить планировщик, управляемый базой данных, в ASP.NET Core?

Я добавил Timer в Startup класс ASP.Net Core приложение. Он срабатывает в течение некоторого периода времени и выполняет такие операции, как запись образца текста. Мне нужно, чтобы он мог выполнять операции на основе базы данных, такие как добавление записи в таблицу. Поэтому я пытаюсь получить AppDbContext от DI, но это всегда ноль. Пожалуйста, смотрите код:

    public class Scheduler
{
    static Timer _timer;
    static bool _isStarted;
    static ILogger<Scheduler> _logger;
    const int dueTimeMin = 1;
    const int periodMin = 1;

    public static void Start(IServiceProvider serviceProvider)
    {
        if (_isStarted)
            throw new Exception("Currently is started");

        _logger = (ILogger<Scheduler>)serviceProvider.GetService(typeof(ILogger<Scheduler>));

        var autoEvent = new AutoResetEvent(false);
        var operationClass = new OperationClass(serviceProvider);
        _timer = new Timer(operationClass.DoOperation, autoEvent, dueTimeMin * 60 * 1000, periodMin * 60 * 1000);
        _isStarted = true;
        _logger.LogInformation("Scheduler started");            
    }
}

public class OperationClass
{
    IServiceProvider _serviceProvider;
    ILogger<OperationClass> _logger;
    AppDbContext _appDbContext;

    public OperationClass(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _logger = (ILogger<OperationClass>)serviceProvider.GetService(typeof(ILogger<OperationClass>));
        _appDbContext = (AppDbContext)_serviceProvider.GetService(typeof(AppDbContext));
    }

    public void DoOperation(Object stateInfo)
    {
        try     
        {
            _logger.LogInformation("Timer elapsed.");

            if (_appDbContext == null)
                throw new Exception("appDbContext is null");

            _appDbContext.PlayNows.Add(new PlayNow
            {
                DateTime = DateTime.Now
            });

            _appDbContext.SaveChanges();
        }
        catch (Exception exception)
        {
            _logger.LogError($"Error in DoOperation: {exception.Message}");
        }
    }
}

И вот это код из Startup:

        public Startup(IHostingEnvironment env, IServiceProvider serviceProvider)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();

        AppHelper.InitializeMapper();
        Scheduler.Start(serviceProvider);
    }

Я думаю, я звоню Scheduler.Start в неправильном месте. Кажется, что AppDbContext еще не готов

Как правильно позвонить Scheduler.Start?

2 ответа

Решение

Когда вы запускаете код в фоновом потоке, вы всегда должны начинать новую "область" для вашего DI-контейнера в этом фоновом потоке и разрешать из этой области.

Итак, что вы должны сделать, это:

  • Создайте новую область внутри события
  • Разрешить OperationClass из этой сферы
  • внутри OperationClass полагаться только на конструктор инъекций; не на месте обслуживания.

Ваш код должен выглядеть примерно так:

public class Scheduler
{
    static Timer _timer;
    const int dueTimeMin = 1;
    const int periodMin = 1;

    public static void Start(IServiceScopeFactory scopeFactory)
    {
        if (scopeFactory == null) throw new ArgumentNullException("scopeFactory");
        _timer = new Timer(_ =>
        {
            using (var scope = new scopeFactory.CreateScope())
            {
                scope.GetRequiredService<OperationClass>().DoOperation();
            }
        }, new AutoResetEvent(false), dueTimeMin * 60 * 1000, periodMin * 60 * 1000);
    }
}

Вот Start зависит от IServiceScopeFactory, IServiceScopeFactory может быть решена из IServiceProvider,

Ваш OperationClass воля становится примерно такой:

public class OperationClass
{
    private readonly ILogger<OperationClass> _logger;
    private readonly AppDbContext _appDbContext;

    public OperationClass(ILogger<OperationClass> logger, AppDbContext appDbContext)
    {
        if (logger == null) throw new ArgumentNullException(nameof(logger));
        if (appDbContext == null) throw new ArgumentNullException(nameof(appDbContext));

        _logger = logger;
        _appDbContext = appDbContext;
    }

    public void DoOperation()
    {
        try     
        {
            _logger.LogInformation("DoOperation.");

            _appDbContext.PlayNows.Add(new PlayNow
            {
                DateTime = DateTime.Now
            });

            _appDbContext.SaveChanges();
        }
        catch (Exception exception)
        {
            _logger.LogError($"Error in DoOperation: {exception}");
        }
    }
}

Хотя эта документация не относится к.NET Core Container, она содержит более подробную информацию о том, как работать с DI-контейнером в многопоточном приложении.

Вы должны позвонить внутрь ConfigureServices после вашего AppDbContext в коде, который вы вызываете, перед регистрацией DI. также вы можете использовать services.BuildServiceProvider() создать IServiceProvider содержащие услуги из предоставленного DI:

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();
    Configuration = builder.Build();

    MmHelper.InitializeMapper();
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>();

    services.AddIdentity<User, IdentityRole>()
        .AddEntityFrameworkStores<AppDbContext>()
        .AddDefaultTokenProviders();

    // ....

    Scheduler.Start(services.BuildServiceProvider());
}
Другие вопросы по тегам