I have a razor pages with some classes such as scheduled tasks that run in the background. I have a IUnitofWork for the databases and uses EF.
In my schedule class "WorkerService : BackgroundService" it does routine backups and other tasks.
How can I reference the Database because I dont have DI due to not implementing razor pages?
Usually this is how I do it using DI on razor code files:
private readonly IUnitOfWork _unitOfWork;
public IndexModel(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
I am new to this DI and been in the udemy and microsoft site daily. I think I have to create a IUnit of work and pass in the ApplicationDbContext maybe in an ovveride? But how to get the context without DI.
Program.cs
builder.Services.AddHostedService<WorkerService>(); //Uses cronos to execute DoWork() every hour
WorkerService.cs
private const string schedule = "*/5 * * * *"; // every 5 for testing
private readonly CronExpression _cron;
public WorkerService()
{
_cron = CronExpression.Parse(schedule);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var utcNow = DateTime.UtcNow;
var nextUtc = _cron.GetNextOccurrence(utcNow);
await Task.Delay(nextUtc.Value - utcNow, stoppingToken);
await DoBackupAsync();
}
}
private static Task DoBackupAsync()
{
DoWork d = new DoWork();
return Task.FromResult("Done");
}
RazorApp/Pages
This is where I need to save data
RazorApp/ScheduledTasks/DoWork.cs
RazorApp/ScheduledTasks/WorkerService.cs
Attempting to DI either the IUnitOfWork or ApplicationDbContext
Further trying different examples like: https://dotnetcorecentral.com/blog/background-tasks
Results in this error as well: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: WebRazor.ScheduledTasks.BackgroundPrinter': Cannot consume scoped service 'WebRazor.DataAccess.ApplicationDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService
public BackgroundPrinter(ILogger<BackgroundPrinter> logger, IWorker worker, ApplicationDbContext dbContext)
{
this.logger = logger;
applicationDbContext = dbContext;
}
Is this where I need to get it from the settings directly or is there a slick way to grab the Db Context?
Ok, wow I was all over the place. I implemented this and it worked.
public BackgroundPrinter(ILogger<BackgroundPrinter> logger, IWorker worker, IServiceProvider serviceProvider)
{
this.logger = logger;
unitOfWork = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<IUnitOfWork>();
}
On construct pass in the Iserviceprovider and access this way. DI to the DoWork and its writing to the database.
Still not sure this is the best way to solve this problem, but I think I have to refactor my unitofwork because every now and then I get an error on writing
"A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext" but this may be easy to solve.
The background tasks with hosted services doc describes how to consume a scoped service in a background task:
Inject IServiceProvider in hosted service's ctor
Use the IServiceProvider to create scope during the execution
Use the scope to resolve required services
Personally for "timed" services like provided WorkerService in most cases I found useful to create scope per every iteration (especially if it uses EF internally). Something along this lines:
private readonly IServiceProvider _serviceProvider;
public WorkerService(IServiceProvider serviceProvider)
{
...
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var utcNow = DateTime.UtcNow;
var nextUtc = _cron.GetNextOccurrence(utcNow);
await Task.Delay(nextUtc.Value - utcNow, stoppingToken);
await DoBackupAsync(stoppingToken);
}
}
private static async Task DoBackupAsync(CancellationToken stoppingToken)
{
using (var scope = _serviceProvider.CreateScope()) // do not forget to dispose the scope
{
var backupService = scope.ServiceProvider.GetRequiredService<IBackupService>();
await backupService.BackUp(stoppingToken);
}
}
Related
I have a class that derives from BackgroundService (IHostedService) for running background tasks. This will be added to my services using builder.Services.AddHostedService<BackgroundTaskService>()
BackgroundService's task runs for the entire duration of the web application, checking for queued data to process.
My question is, how do I instantiate an instance of DbContext from this code?
I could have the BackgroundTaskService constructor accept a DbContext. But wouldn't that keep the DbContext open forever?
And how else could I instantiate it without duplicating all the code to scan my settings file for the connection string, etc.?
The recemmended approach is to inject IDbContextFactory<TContext> as described in the following article: Using a DbContext factory (e.g. for Blazor)
Some application types (e.g. ASP.NET Core Blazor) use dependency injection but do not create a service scope that aligns with the desired DbContext lifetime. Even where such an alignment does exist, the application may need to perform multiple units-of-work within this scope. For example, multiple units-of-work within a single HTTP request.
In these cases, AddDbContextFactory can be used to register a factory for creation of DbContext instances.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Test"));
}
Then in your controller:
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
You can use scope service factory. Check here for reference.
Here you have an example:
// Injection
public class DataApi : BackgroundService
{
private readonly ILogger<DataApi> logger;
private readonly IServiceScopeFactory scopeFactory;
public DataApi(ILogger<DataApi> _logger, IConfiguration _cfg, IServiceScopeFactory _sSF)
{
logger = _logger;
scopeFactory = _sSF;
// e.g. data from appsettings.json
// var recovery = _cfg["Api:Recovery"];
}
// ...
// Usage
protected async Task DataCollector()
{
logger.LogInformation("Collector");
using (var scope = scopeFactory.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var myList = await db.MyEntity
.AsNoTracking()
.Where(t => t.active)
.ToListAsync();
if (myList.Count == 0)
{
logger.LogInformation("Empty...");
return;
}
// logic...
}
await Task.CompletedTask;
}
I have created a .Net Core MVC project and understand that how the dependency injection works for our MVC controller as shown below, but same like I wanted to create an object for my own class by calling the same injected interface/class as a parameter.
public class ShiftsController : BaseController
{
ShardingDbContext _dbContext;
public ShiftsController(ShardingDbContext ShardingDbContext) : base(ShardingDbContext)
{
_dbContext = ShardingDbContext;
ViewBag.Menu = BuildMenu();
}
I have injected the DbContext into my Startup.cs file as below,
//Entity Framework Core
services.AddDbContext<ShardingDbContext>(options => options.UseSqlServer(ConnectionString),
ServiceLifetime.Transient);
The ShiftsController is a C#-MVC controller and the DbContext is working perfectly when I run my app and go to Shift's page in my application, but when I try like below-given code, it's not working and gives an error. So I don't know how to pass the registered class's object while creating an object by using "new" keyword.
public class JobScheduler
{
ShardingDbContext _dbContext;
public JobScheduler(ShardingDbContext ShardingDbContext)
{
_dbContext = ShardingDbContext;
}...
This is my own class and tried to create an object for the class JobScheduler as shown below.
JobScheduler jobs = new JobScheduler();
So now I don't know how to pass the EF core's DbContext's object to the constructor JobScheduler, the DI works fine for the controller but not for a normal class. Can anyone help with this and I am eagerly waiting to understand this logic as well?.
Register your JobScheculer like this:
services.AddSingleton<JobScheduler>();
then use your dbContext like this:
public class JobScheduler
{
private readonly IServiceProvider provider;
public JobScheduler(IServiceProvider provider)
{
}...
public (or private etc) DoYourJob()
{
using (var scope = provider.CreateScope())
{
var dbContext = scope.GetService<ShardingDbContext>();
//use it here
}
}
At the end of the ConfigureServices method of the Startup.cs class, and I did not change anything in the JobSchedulerclass and passing the DbContext object from the service provider as shown below, thanks to everyone who tried to help with this question.
public void ConfigureServices(IServiceCollection services)
{
...
...
JobScheduler job = new
JobScheduler(services.BuildServiceProvider().CreateScope().ServiceProvider
.GetService<ShardingDbContext>());
job.Start();
}
You are right: Your DI works fine but your ShardingDbContext is not passed into your JobScheduler because you are not using DI to instanciate JobScheduler. Whenever you are explicitly creating an object instance using the new keyowrd you are not using DI.
You have two options:
Wherever you are calling new JobScheduler() let DI inject you a ShardingDbContext through the constructor and pass it to JobScheduler like so new JobScheduler(shardingDbContext)
Register JobScheduler to the dependency injection as well and let DI build up the whole chain so you don't need to call new JobScheduler() but rather get a JobScheduler injected directly wherever you need it
Edit
As requested here is the example for a timed job using a short lived DB context:
public class TimedBackgroundService : IHostedService, IDisposable
{
private readonly Timer timer;
private readonly IServiceProvider serviceProvider;
public TimedBackgroundService(IServiceProvider serviceProvider)
{
timer = new Timer(async state => await ExecuteAsync());
this.serviceProvider = serviceProvider;
}
public Task StartAsync(CancellationToken stoppingToken)
{
timer.Change(0, TimeSpan.FromMinutes(30));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
timer.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose() => timer.Dispose();
private async Task ExecuteAsync()
{
try
{
using var scope = serviceProvider.CreateScope();
var job = scope.ServiceProvider.GetRequiredService<MyJob>();
await job.Execute();
}
catch (Exception exception)
{
// log error here
return;
}
}
}
The MyJob class wil look something like this:
public class MyJob
{
private readonly ShardingDbContext dbContext;
public MyJob(ShardingDbContext dbContext)
{
this.dbContext = dbContext;
}
public Task Execute()
{
// Your logic goes here
}
}
Then you register your classes in the startup like so:
services
.AddHostedService<TimedBackgroundService>()
.AddScoped<MyJob>();
Now you have a job which runs every 30 minutes and uses a short lived db context.
Various examples online show injecting a DbContext into a gRPC service in the service constructor:
public ImageStorageService(ILogger<ImageStorageService> logger, SqliteDbContext dbContext)
{
_logger = logger;
_dbContext = dbContext;
}
and using that _dbContext in all the gRPC implementation methods. This doesn't seem right - I'm new to EF and .Net Core web in general but a DbContext is not thread-safe and multiple gRPC methods can be executing at the same time, yes?
Anyway I found another way, here's the constructor and an example api method implementation:
public ImageStorageService(ILogger<ImageStorageService> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
public override async Task<GetImageCountReply> GetImageCount(GetImageCountRequest request, ServerCallContext context)
{
using var scope = _serviceScopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SqliteDbContext>();
var count = await dbContext.StoredImages.CountAsync(context.CancellationToken);
return new GetImageCountReply { Count = count };
}
Am I over-complicating things? services.AddDbContext<> injects database contexts with Scoped lifetime so this is necessary, isn't it?
I have made an implementation of IHostedService where I will load a cache when my application is starting up. In that implementation of IHostedService I need the Context for the DB, but it is null when I try to get it.
I am loading my service in StartUp ConfigureServices in the last line:
services.AddDbContext<MyContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection")));
...
services.AddHostedService<InitializeCacheService>();
And my service look like this (I am getting the cache but not the context):
public class InitializeCacheService : IHostedService
{
private readonly IServiceScopeFactory _scopeFactory;
public InitializeCacheService(IServiceScopeFactory scopeFactory)
{
this._scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken cancellationToken)
{
using (var scope = _scopeFactory.CreateScope())
{
var cache = scope.ServiceProvider.GetService<IMemoryCache>();
var context = scope.ServiceProvider.GetRequiredService<MyContext>();
CacheLogic cacheLogic = new CacheLogic(cache, context);
cacheLogic.LoadProfileCache();
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
What am I doing wrong?
Thanks very much in advance :-)
UPDATE
This is actually working, the problem is that I missed the initialization of the context in CacheLogic :-| Sorry about that!
But now it is working then i get the error below when I query in CacheLogic, an that might be because it is async and I handle over the Context in a time limited scope in the InitializeCacheService?
But After rethinking my solution I think that the CacheLogic shall be a singleton, and then my problem is how to get access to cache and DBContext in a singleton (I will create a new question for that).
System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
ObjectDisposed_ObjectName_Name'
This question already has answers here:
How to consume a Scoped service from a Singleton?
(2 answers)
Closed 4 years ago.
I'm creating an ASP.NET Core application. It is using Entity Framework Core for database access. I'm using services.AddDbContext in Startup.cs and the DB Context is injected into my controller as expected.
I am also have a background task using IHostedService that is added as a Singleton. I'd like to have an instance of my DBContext in my implementation of IHostedService. When I try to do this I get a run time error that my IHostedService cannot consume a scoped service (my DB Context).
The DB Context class takes a parameter of DbContextOptions options and passes the options to the base constructor (DbContext).
I need to create an instance of my DB Context in my implementation of IHostedService (a singleton object) but I can't seem to figure out how to correctly create a new instance of DbContextOptions from my IHostedService implementation.
For resolving a Scoped Service from a Singleton Service, you could create the scoped service from IServiceProvider.
Here is the demo code:
public class DbHostedService : IHostedService
{
private readonly ILogger _logger;
public DbHostedService(IServiceProvider services,
ILogger<DbHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is starting.");
DoWork();
return Task.CompletedTask;
}
private void DoWork()
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var context =
scope.ServiceProvider
.GetRequiredService<ApplicationDbContext>();
var user = context.Users.LastOrDefault();
_logger.LogInformation(user?.UserName);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
return Task.CompletedTask;
}
}
Reference: Consuming a scoped service in a background task
I think you need not IHostedService. Myeby your need this
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<BlogContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
services.AddSession();
}