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

问题描述:

我已将Timer添加到Startup类的ASP.Net Core应用程序。它会在一段时间内触发并执行诸如记录示例文本等操作。我需要它能够执行数据库驱动的操作,如向表中添加记录。所以我尝试从DI获得AppDbContext,但它始终为空。请参阅代码:什么是在ASP.NET Core中添加数据库驱动调度程序的正确位置?

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

+1

是的,你是在启动流程太早调用它。直到ConfigureServices被调用之后,服务提供者才能正确配置。所以我建议你在添加数据库上下文之后再看看那里。我还建议使调度程序具有明确的依赖关系并将其放入服务集合中。 – Nkosi

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

所以,你应该做的是:

  • 从范围事件
  • 解决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核心容器,this documentation提供了有关如何使用DI容器在多线程工作的更详细信息应用。

+0

感谢您的描述性答案。我应该在哪里调用'Scheduler.Start()'? –

+0

@Afshar:你应该在[Composition Root](http://blog.ploeh.dk/2011/07/28/CompositionRoot/)中的某个地方调用'Start'。你的'Startup'方法似乎是一个不错的地方。 – Steven

你的AppDbContext已经解决之后,在DI注册之前调用它的代码中,你应该在ConfigureServices之内调用它。你也可以使用services.BuildServiceProvider()从所提供的DI创建一个包含IServiceProvider服务:

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()); 
}