在 ASP.NET Core 中添加数据库驱动调度程序的正确位置是什么?

What is the correct place to add a database driven scheduler in ASP.NET Core?

我在 ASP.Net Core 应用程序的 Startup class 中添加了 Timer。它会在某个时间段触发并执行诸如记录示例文本之类的操作。我需要它能够执行数据库驱动的操作,例如将记录添加到 table。所以我尝试从 DI 获取 AppDbContext 但它始终为空。请看代码:

    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 的正确调用位置是什么?

当您 运行 在后台线程上编写代码时,您应该始终在该后台线程上为您的 DI 容器开始一个新的 'scope' 并从该范围解析。

所以你应该做的是:

  • 在事件中创建一个新范围
  • 从该范围解析 OperationClass
  • 里面OperationClass只依赖构造函数注入;不在 Service Location.

您的代码应如下所示:

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取决于IServiceScopeFactoryIServiceScopeFactory 可以从 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 容器的文档,this documentation 提供了有关如何在多线程应用程序中使用 DI 容器的更详细信息。

您应该在 AppDbContext 解析后在 ConfigureServices 中调用它,在 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());
}