I have added a Timer
to Startup
class of an ASP.Net Core
application. It fires on some time period and do operations like logging a sample text. I need it to be able to do database driven operations like adding a record to a table. So I try to get AppDbContext
from DI but it is always null. Please see the code:
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}");
}
}
}
And here it is the code from 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);
}
I guess I am calling Scheduler.Start
in a wrong place. Seems that AppDbContext
is not ready yet.
What is the correct place to call Scheduler.Start
?
When you are running code on a background thread, you should always begin a new 'scope' for your DI container on that background thread and resolve from that scope.
So what you should do is:
OperationClass
from that scopeOperationClass
only rely on constructor injection; not on Service Location.Your code should look something like this:
Here
Start
depends onIServiceScopeFactory
.IServiceScopeFactory
can be resolved from theIServiceProvider
.Your
OperationClass
will becomes something like the following:Although not documentation particular to the .NET Core Container, this documentation provides a more detailed information about how to work with a DI Container in a multi-threaded application.
You should call it inside
ConfigureServices
after yourAppDbContext
has been resolved, in the code you are calling it before DI registrations. also you can useservices.BuildServiceProvider()
to create anIServiceProvider
containing services from the provided DI: