How to start an ASP.NET Core BackgroundService on demand? - asp.net-core

I want to be able to start fire-and-forget jobs in ASP.NET Core 2.2. I have tried the following:
services.AddHostedService<TestHostedService>();
public class TestHostedService : BackgroundService
{
private readonly ILogger _logger;
public TestHostedService(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TestHostedService>();
}
public IBackgroundTaskQueue TaskQueue { get; }
protected async override Task ExecuteAsync(
CancellationToken cancellationToken)
{
_logger.LogInformation("TestHostedService is starting.");
_logger.LogInformation("TestHostedService is stopping.");
}
}
However, this automatically starts and I want to be able to start it on demand, similarly to how Hangfire allows:
BackgroundJob.Enqueue<TestJob>(x => x.DoWork());
This also allows the job to naturally use ASP.NET CORE DI.
Question: How to start an ASP.NET Core BackgroundService on demand?
###Background information
I am dealing with an application that needs to fire-and-forget various methods. The already written code looks like this:
Task.Run(() => RunSomething(_serviceScopeFactory));
This means that each method must explicitly deal with getting a scope and retrieving the dependencies which is quite ugly.

If you want to run the BackgroundService in the MVC controller or other service. You could try to inject the IServiceProvider to that class and then loop all the hosted service and find the background service, at last you could call the startasync method.
More details, you could refer to below codes:
Register the service in Startup.cs
services.AddHostedService<TestHostedService>();
Execute the background service in the controller:
public class HomeController : Controller
{
private readonly IServiceProvider _serviceProdiver;
public HomeController(IServiceProvider serviceProdiver) {
_serviceProdiver = serviceProdiver;
}
public async Task<IActionResult> Index()
{
var allBackgroundServices = _serviceProdiver.GetServices<IHostedService>();
foreach (var hostedService in allBackgroundServices)
{
if (hostedService.GetType() == typeof(TestHostedService))
{
await hostedService.StartAsync(CancellationToken.None);
}
}
return View();
}
}
Result:

Related

Error accessing a service that uses DbContext on my quartz job

I am making a web API using ASP.NET Core and now I am having a problem with quartz scheduled jobs. The jobs I have will access my services to update the database. After some researches, I figured how to do the dependency injection so that my jobs can access the services, here is how I overrode the job factory:
public class AspNetCoreJobFactory : SimpleJobFactory
{
IServiceProvider _provider;
public AspNetCoreJobFactory(IServiceProvider provider)
{
_provider = provider;
}
public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
return (IJob)this._provider.GetService(bundle.JobDetail.JobType);
}
catch(Exception e)
{
throw new SchedulerException(string.Format("Problem while instantiating job '{0}' from the AspNet Core IOC.", bundle.JobDetail.Key), e);
}
}
}
and I added this line on my startup configure:
_quartzScheduler.JobFactory = new AspNetCoreJobFactory(app.ApplicationServices);
Lastly I added those two lines on my ConfigureServices method:
services.AddSingleton<IUserService, UserService>();
services.AddTransient<BatchJobCheckContract>();
right now I am getting this exception when trying to execute the job, it seems like it's because my service uses the DbContext, how can I solve this?
Cannot consume scoped service 'RHP.data.RHPDbContext' from singleton
'RHP.data.IServices.Administration.IUserService'.
After playing around with Quartz (version 3.2.3), it looks like you do not have to write your own JobFactory to use Microsoft DI. (See ASP.NET Core Integration and Microsoft DI Integration):
Add the Quartz.AspNetCore nuget package and you can scoped services like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IScopedService, ScopedService>();
// Job has scoped dependencies, so it must be scoped as well
services.AddScoped<Job>();
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionScopedJobFactory();
var jobKey = new JobKey("job");
q.AddJob<Job>(jobKey);
q.AddTrigger(t => /* ... */));
});
services.AddQuartzServer(opts => opts.WaitForJobsToComplete = true);
}
However, if you cannot use the current version of Quartz.AspNetCore, you could still
use IServiceProvider as dependency in your Job class and resolve services there:
public class Job : IJob
{
private readonly IServiceProvider _serviceProvider;
public Job(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task Execute(IJobExecutionContext context)
{
using var scope = _serviceProvider.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
// ...
}
}
Like this the Job class controls the lifetime of scoped services.
I've previously had a similar problem with background tasks, you might need to create a scope.
I've adapted this code and applied it to your use case.
public class AspNetCoreJobFactory : SimpleJobFactory
{
IServiceProvider _provider;
public AspNetCoreJobFactory(IServiceProvider provider)
{
_provider = provider;
}
public override IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
try
{
using(var serviceScope = _provider.CreateScope())
{
var services = serviceScope.ServiceProvider.
return (IJob)services.GetService(bundle.JobDetail.JobType);
}
}
catch(Exception e)
{
throw new SchedulerException(string.Format("Problem while instantiating job '{0}' from the AspNet Core IOC.", bundle.JobDetail.Key), e);
}
}
}

Implementing service worker in existing ASP.NET Core MVC application

I'm developing an ASP.NET Core MVC web application where I have these two tasks that should be running as background services:
Set the user status as "Expired" if EndOfSubscription date is == DateTime.Now
Before 1 month of EndOfSubscription date send a reminder e-mail to this user
After searching, I found that I can use service worker to implement this. But I'm totally confused how to use this service worker in existing ASP.NET Core MVC web application where I need to access my models and database.
Should I isolate these tasks in a separate service worker project? But in this case should I share the same database for both projects?
Can someone guide me with main steps in this kind of situations?
Thank you in advance.
Service worker or Worker service?
A Service Worker is a way to run background tasks in a browser and definitely unsuitable if you want to execute something on the server.
A Worker service is essentially a template with the (few) calls needed to run a BackgroundService/IHostedService in a console application and (optionally, through extensions) as a Linux daemon or Windows service. You don't need that template to create and run a BackgroundService.
The tutorial Background tasks with hosted services in ASP.NET Core shows how to create and use a BackgroundService but is a bit ... overengineered. The article tries to show too many things at the same time and ends up missing some essential things.
A better introduction is Steve Gordon's What are Worker Services?.
The background service
All that's needed to create a background service, is a class that implements the IHostedService interface. Instead of implementing all the interface methods, it's easier to inherit from the BackgroundService base class and override just the ExecuteAsync method.
The article's example shows this method doesn't need to be anything fancy:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
That's just a loop with a delay. This will run until the web app terminates and signals the stoppingToken. This service will be created by the DI container, so it can have service dependencies like ILogger or any other singleton service.
Registering the service
The background service needs to be registered as a service in ConfigureServices, the same way any other service is registered. If you have a console application, you configure it in the host's ConfigureServices call. If you have a web application, you need to register it in Startup.ConfigureServices:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<OrdersContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
...
//Register the service
services.AddHostedService<Worker>();
services.AddRazorPages();
}
This registers Worker as a service that can be constructed by the DI container and adds it to the list of hosted services that will start once .Run() is called in the web app's Main :
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
Using DbContext and other scoped services
Adding a DbContext as a dependency is trickier, since DbContext is a scoped service. We can't just inject a DbContext instance and store it in a field - a DbContext is meant to be used as a Unit-of-Work, something that collects all changes made for a single scenario and either commit all of them to the database or discard them. It's meant to be used inside a using block. If we dispose the single DbContext instance we injected though, where do we get a new one?
To solve this, we have to inject the DI service, IServiceProvider, create a scope explicitly and get our DbContext from this scope:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceProvider _services;
//Inject IServiceProvider
public Worker(IServiceProvider services, ILogger<Worker> logger)
{
_logger = logger;
_services=services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//Create the scope
using (var scope = _services.CreateScope())
{
//Create OrdersContext in the scope
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}
//OrdersContext will be disposed when exiting the scope
...
}
}
}
The OrdersContext will be disposed when the scope exits and any unsaved changes will be discarded.
Nothing says the entire code needs to be inside ExecuteAsync. Once the code starts getting too long, we can easily extract the important code into a separate method :
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
using (var scope = _services.CreateScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
await DoWorkAsync(ctx,stoppingToken);
}
await Task.Delay(1000, stoppingToken);
}
}
private async Task DoWorkAsync(OrdersContext ctx,CancellationToken stoppingToken)
{
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}

how to handle DbContext lifetime in API / backgroundworker

the past days I have been struggling with injecting a DbContext in MY background worker. On the one hand, I want to inject the dbContext in my backgorund worker, but on the other hand I also want to use it in my API.
The injecting in my API seems to work fine, but since my worker is a singleton, I can not follow the standard lifetime of scoped for my dbcontext, and I have to add it as transient.
I have already tried to create a unit of work, in which I can refresh the context myself in my worker, effectively creating some kind of scoped service. I would refresh the context every time the worker went through his loop once again. This worked, and the application was working as I wanted, but I was no longer able to properly test, since I would create a new DbContext myself in the code. I feel like there must be a better way for to handle this.
My project structure looks like the following:
API => contains controlers + models I use for post requests.
The API project needs to use my database, to get and post data. It uses the repositories for this
Core (class library) => contains some core models
Domain(class library) => Contains my domain models + repositories.
All database work goes through here
Worker => Contains some logic.
The worker needs to use my database, to get and post data. It uses the repositories for this
Services (class library) => Some services that contain some logic.
The worker uses my repositories to get to the database
Tests => Tests for all code.
I want to be able to to integrationTesting as well here.
I currently inject all repositories and services in both my API and worker:
Worker configureservices:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddDbContext<CatAPIDbContext>(ServiceLifetime.Transient);
services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
services.AddTransient(typeof(IMotorController), typeof(MotorController));
services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient(typeof(IFeedingTimeChecker), typeof(FeedingTimeChecker));
services.AddHostedService<Worker>();
});
(EDIT)Worker code:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public IFeedingTimeChecker _feedingTimeChecker { get; }
public Worker(ILogger<Worker> logger, IFeedingTimeChecker feedingTimeChecker)
{
_logger = logger;
_feedingTimeChecker = feedingTimeChecker;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
_feedingTimeChecker.ResetFeedingTimesGivenIfNeeded();
_feedingTimeChecker.FeedIfNeeded();
}
catch(Exception ex)
{
_logger.LogError(ex.ToString());
}
await Task.Delay(10000, stoppingToken);
}
}
}
(EDIT)FeedingTimeChecker (called by worker)
private FeedingProfile _currentProfile { get; set; }
public DateTime lastResetDataFeedingTimes;
public DateTime lastProfileRefresh;
private readonly ILogger<FeedingTimeChecker> _logger;
private IFeedingProfileRepository _feedingProfileRepository { get; set; }
private IFeedingTimesRepository _feedingTimesRepository { get; set; }
private IFoodDispenser _foodDispenser { get; }
public FeedingTimeChecker(IFeedingProfileRepository feedingProfileRepository, IFeedingTimesRepository feedingTimesRepository,IFoodDispenser foodDispenser, ILogger<FeedingTimeChecker> logger)
{
lastResetDataFeedingTimes = DateTime.MinValue.Date;
lastProfileRefresh = DateTime.MinValue.Date;
_foodDispenser = foodDispenser;
_logger = logger;
_feedingTimesRepository = feedingTimesRepository;
_feedingProfileRepository = feedingProfileRepository;
}
public void UpdateCurrentProfile()
{
if(Time.GetDateTimeNow - TimeSpan.FromSeconds(5) > lastProfileRefresh)
{
_logger.LogInformation("Refreshing current profile");
_currentProfile = _feedingProfileRepository.GetCurrentFeedingProfile();
lastProfileRefresh = Time.GetDateTimeNow;
}
}
API configureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddDbContext<CatAPIDbContext>();
services.AddTransient(typeof(IFeedingProfileRepository), typeof(FeedingProfileRepository));
services.AddTransient(typeof(IFeedingTimesRepository), typeof(FeedingTimesRepository));
services.AddTransient(typeof(IFeedHistoryRepository), typeof(FeedHistoryRepository));
services.AddTransient(typeof(IMotorController), typeof(MotorController));
services.AddTransient(typeof(IFoodDispenser), typeof(FoodDispenser));
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
}
in my repositories I use the dbContext like the following:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
public CatAPIDbContext _dbContext { get; set; }
public GenericRepository(CatAPIDbContext dbContext)
{
_dbContext = dbContext;
}
public T GetById(object id)
{
return _dbContext.Set<T>().Find(id);
}
}
The result I would expect, is for my worker and API to behave correctly, always getting the lastest data and disposing of the dbContext on every single request, since I use a transient lifetime for my dbContext.
However, in my worker, I always get the following error:
The instance of entity type 'FeedingTime' cannot be tracked because another instance another instance of this type with the same key is already being tracked.
This error occurs when I try to set a column in the FeedingTime table.
A feedingProfile has 0-many feedingTimes, and the feedingProfile constantly retrieved.
Any solution where I can keep a testable clean codebase, but yet not run into this problem would be very welcome.
Thanks in advance

How to send constantly updates using .Net Core SignalR?

I am new to SignalR and I would like to build such app -- every second a hub sends current time to all connected clients.
I found tutorial, but it is for .Net Framework (not Core): https://learn.microsoft.com/en-us/aspnet/signalr/overview/getting-started/tutorial-high-frequency-realtime-with-signalr So on one hand I don't know how to translate it to .Net Core SignalR, on the other hand I don't know how to write it from scratch (the limiting condition is the fact a hub is a volatile entity, so I cannot have state in it).
I need something static (I guess) with state -- let's say Broadcaster, when I create some cyclic action which in turn will send updates to clients. If such approach is OK, how to initialize this Broadcaster?
Currently I added such static class:
public static class CrazyBroadcaster
{
public static void Initialize(IServiceProvider serviceProvider)
{
var scope = serviceProvider.CreateScope();
var hub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
var sub = Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ => hub.Clients.All.SendAsync("Bar", DateTimeOffset.UtcNow));
}
}
Yes, I know it is leaky. I call this method at the end of Startup.Configure, probably tons of violations here, but so far it is my best shot.
The missing piece was hosted service, i.e. the code that runs in the background -- https://learn.microsoft.com/en-US/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2.
So my crazy class is now transformed into:
public sealed class HostedBroadcaster : IHostedService, IDisposable
{
private readonly IHubContext<ChatHub> hubContext;
private IDisposable subscription;
public HostedBroadcaster(IHubContext<ChatHub> hubContext)
{
this.hubContext = hubContext;
}
public void Dispose()
{
this.subscription?.Dispose();
}
public Task StartAsync(CancellationToken cancellationToken)
{
this.subscription = Observable.Interval(TimeSpan.FromSeconds(1)).Subscribe(_ => hubContext.Clients.All.SendAsync("Bar", DateTimeOffset.UtcNow));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.subscription?.Dispose();
return Task.CompletedTask;
}
}

Unable to get Scoped Service in aspnetcore 1 - RC1 to work

My scoped service for some reason seems to be generating different instances of the same class when I try to access it in 2 middlewares within the same request.
Scenario: I am adding a scoped service as such:
public interface ISimplyRecorder
{
void AddInfo(string key, string value);
Dictionary<string, string> GetAllInfo();
}
public class SimplyCoreRecorderService : ISimplyRecorder
{
private Dictionary<string,string> data;
public SimplyCoreRecorderService()
{
data = new Dictionary<string, string>();
}
public void AddInfo(string key,string value)
{
data.Add("",value);
}
public Dictionary<string,string> GetAllInfo()
{
return data;
}
}
and then the following in startup.cs
services.AddScoped<ISimplyRecorder,SimplyRecorderService>();
now I am calling this service in the constructor of a sample Middleware. I am able to access the service with a new instance and add data into it and then I call await _next(context). However, when I am calling the service in my HomeController, MVC which follows the middleware above, I seem to be getting a new instance of the service even though it's the same request.
HomeController:
ISimplyRecorder _simply;
private IHostingEnvironment _env;
public HomeController(IHostingEnvironment env,ISimplyRecorder simply)
{
_simply = simply;
_env = env;
}
public IActionResult Index()
{
_simply.AddInfo("Home:Action","resulted in index action");
return View();
}
complete code available at: https://github.com/muqeet-khan/SimplyCore if someone wants to give it a go.
Middlewares are instantiated only once when it's first involved, then all the following requests are handled by that middleware instance. NOT a new middleware instance for each request.
You get your ISimplyRecorder in the constructor of the middleware and "cache" it as a private readonly variable. This means the middleware will get the ISimplyRecorder instance of the first request, then keep adding data to that instance for all the following requests rather than the new ISimplyRecorder instance for the following requests which you get in HomeController.
To solve it, you need to get ISimplyRecorder instance from the Invoke method of the middleware.
// using Microsoft.Extensions.DependencyInjection;
public async Task Invoke(HttpContext httpContext)
{
ISimplyRecorder recoder = httpContext.RequestServices.GetRequiredService<ISimplyRecorder>();
}
EDIT:
The comment of Juergen is correct, I tried it out. You may also just write like this:
public async Task Invoke(HttpContext httpContext, ISimplyRecorder recorder)
{
// recorder is from DI
}