I have an .NET Core 3.1 app that serves an endpoint that describes health of application, and an IHostedService crunching through data in database.
There's a problem though, the worker function of HostedService starts processing for a long time, and as result the Configure() method in Startup is not called and the /status endpoint is not running.
I want the /status endpoint to start running before the HostedService kicks off. How do i start the endpoint before the Hosted Service?
Sample code
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<SomeHostedProcessDoingHeavyWork>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/status", async context =>
{
await context.Response.WriteAsync("OK");
});
});
}
}
The HostedService
public class SomeHostedProcessDoingHeavyWork : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await MethodThatRunsForSeveralMinutes();
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
private async Task MethodThatRunsForSeveralMinutes()
{
// Process data from db....
return;
}
}
I tried to explore adding the HostedService in Configure(), but app.ApplicationServices is a ServiceProvider hence readonly.
I think proposed solutions are a kind of workarounds.
If you add your hosted service inside ConfigureServices(), it will be started before Kestrel because the GenericWebHostService (that in fact runs Kestrel), is added in Program.cs when you call
.ConfigureWebHostDefaults(webBuilder =>
webBuilder.UseStartup<Startup>()
)
so it's always being added as lasts.
To launch your hosted service after Kestrel, just chain another call to
.ConfigureServices(s => s.AddYourServices())
after the call to ConfigureWebHostDefaults().
Something like this:
IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
.ConfigureServices(s => {
s.AddHostedService<SomeHostedProcessDoingHeavyWork>();
});
and you should be done.
ExecuteAsync should return a Task and it should do so quickly. From the documentation (emphasis mine)
ExecuteAsync(CancellationToken) is called to run the background
service. The implementation returns a Task that represents the entire
lifetime of the background service. No further services are started
until ExecuteAsync becomes asynchronous, such as by calling await.
Avoid performing long, blocking initialization work in ExecuteAsync.
The host blocks in StopAsync(CancellationToken) waiting for
ExecuteAsync to complete.
You should be able to get around this by moving your logic into a seperate method and awaiting that
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await MethodThatRunsForSeveralMinutes();
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
Alternatively you might just be able to add an await at the start of the method:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
while (!stoppingToken.IsCancellationRequested)
{
await MethodThatRunsForSeveralMinutes();
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
I ended up using Task.Yield() and implementing an abstract class to encapsulate it, with optional PreExecuteAsyncInternal hook and errorhandler ExecuteAsyncExceptionHandler
public abstract class AsyncBackgroundService : BackgroundService
{
protected ILogger _logger;
private readonly TimeSpan _delay;
protected AsyncBackgroundService(ILogger logger, TimeSpan delay)
{
_logger = logger;
_delay = delay;
}
public virtual Task PreExecuteAsyncInternal(CancellationToken stoppingToken)
{
// Override in derived class
return Task.CompletedTask;
}
public virtual void ExecuteAsyncExceptionHandler(Exception ex)
{
// Override in derived class
}
public abstract Task ExecuteAsyncInternal(CancellationToken stoppingToken);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Prevent BackgroundService from locking before Startup.Configure()
await Task.Yield();
_logger.LogInformation("Running...");
await PreExecuteAsyncInternal(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ExecuteAsyncInternal(stoppingToken);
await Task.Delay(_delay, stoppingToken);
}
catch (TaskCanceledException)
{
// Deliberate
break;
}
catch (Exception ex)
{
_logger.LogCritical($"Error executing {nameof(ExecuteAsyncInternal)} in {GetType().Name}", ex.InnerException);
ExecuteAsyncExceptionHandler(ex);
break;
}
}
_logger.LogInformation("Stopping...");
}
}
await Task.Yield didn't work for me.
The simplest obvious solution:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Implementation omitted
services.AddSingleton<ApplicationRunState>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Implementation omitted
app.MarkConfigurationAsFinished();
}
}
StartupExtensions.cs
public static void MarkConfigurationAsFinished(this IApplicationBuilder builder)
{
builder.ApplicationServices.GetRequiredService<ApplicationRunState>()
.ConfigurationIsFinished = true;
}
ExampleBackgroundService.cs
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (!_serviceProvider.GetRequiredService<ApplicationRunState>()
.ConfigurationIsFinished)
{
await Task.Delay(5000);
continue;
}
// Further implementation omitted
}
}
Related
I have tried to follow this tutorial on how to create a backgroundworker.
Most of the stuff wasn't useful for me, so I didn't include that. I have no need for a queue. I just need to have this backgroundworker running in the background doing stuff every X hours
My worker looks like this. Unfortunately it seems like it never calls the ExecuteAsync method
public class EnergySolutionBackgroundWorker : BackgroundService
{
private readonly ILogger<EnergySolutionBackgroundWorker> _logger;
public EnergySolutionBackgroundWorker(ILogger<EnergySolutionBackgroundWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("{Type} is now running in the background.", nameof(BackgroundWorker));
await BackgroundProcessing(stoppingToken);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogCritical("The {Type} is stopping due to a host shutdown.", nameof(BackgroundWorker));
return base.StopAsync(cancellationToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await Task.Delay(new TimeSpan(0, 0, 1), stoppingToken);
// Doing some tasks
}
catch (Exception ex)
{
_logger.LogCritical("An error occurred when publishing a book. Exception: {#Exception}", ex);
}
}
}
}
In Startup.cs I have the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHostedService<EnergySolutionBackgroundWorker>();
}
From my understanding, this should be enough for it to automatically start the backgroundworker during startup, but that is not the case. What am I doing wrong?
you can add timer in start function.
A timed background task makes use of the System.Threading.Timer class. The timer triggers the task's DoWork method. The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
there is a reference
I believe I found the answer! Startup.cs is not run before the first call to the API is made. After calling a simple Test method in one of my controllers, the BackgroundProcessing method was called
That's a bit annoying, as I was hoping I later could create a backgroundworker that loads a lot of data into memory instead of it happening when the first call is made
I have a .net Core 3.0 BackgroundService application that works fine when running in console mode, but once i deploy as a service the configuration object that should be loaded from appsettings.json is empty. What gives?
Program.cs
public class Program
{
public static async System.Threading.Tasks.Task Main(string[] args)
{
var hostbuilder = new HostBuilder()
.UseWindowsService()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location))
.AddJsonFile("appsettings.json");
})
.ConfigureLogging(
options => options.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Information))
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Importer>().Configure<EventLogSettings>(config =>
{
config.LogName = "Application";
config.SourceName = "Importer";
});
});
#if (DEBUG)
await hostbuilder.RunConsoleAsync();
#else
await hostbuilder.RunAsServiceAsync();
#endif
}
}
Extension Method for IhostBuilder to run service
public static class ServiceBaseLifetimeHostExtensions
{
public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
}
public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
}
}
ServiceBaseLifetime class to handle service lifecycle
public class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();
public ServiceBaseLifetime(IHostApplicationLifetime applicationLifetime)
{
ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
}
private IHostApplicationLifetime ApplicationLifetime { get; }
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
cancellationToken.Register(() => _delayStart.TrySetCanceled());
ApplicationLifetime.ApplicationStopping.Register(Stop);
new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
return _delayStart.Task;
}
private void Run()
{
try
{
Run(this); // This blocks until the service is stopped.
_delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
}
catch (Exception ex)
{
_delayStart.TrySetException(ex);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
Stop();
return Task.CompletedTask;
}
// Called by base.Run when the service is ready to start.
protected override void OnStart(string[] args)
{
_delayStart.TrySetResult(null);
base.OnStart(args);
}
// Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
// That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
protected override void OnStop()
{
ApplicationLifetime.StopApplication();
base.OnStop();
}
}
The actual implementation of the service is irrelevant other than the constructor, which takes the logger and configuration through DI.
private readonly ILogger<Importer> _logger;
private readonly IConfiguration _configuration;
public Importer(IConfiguration configuration, ILogger<Importer> logger)
{
_logger = logger;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"Why is {_configuration["Key1"]} empty?");
}
appsettings.json
{
"Key1":"some value"
}
When i run through debug the console app starts up and runs and logs and has the configuration loaded from appsettings. When i deploy as a service the configuration object is empty.
Notes: The appsettings file is being read, i can tell this by changing the name of it and it throws an exception for file not found. The appsettings file is also not empty.
My issue appears to be some kind of async race condition problem (I am guessing, not positive). The first tick through ExecuteAsync the configuration is not loaded, but the second time through it is. I had the service dying if it encountered that exception, so i never got it to tick a second time.
This appears to be an XY problem and is worthy of a refactor
Create a strongly typed model to hold the desired settings
public class ImporterSettings {
public string Key1 { get; set; }
}
Refactor hosted service to depend on the settings since tightly coupling services to IConfiguration is a code smell in my opinion
private readonly ILogger<Importer> _logger;
private readonly ImporterSettnigs settings;
public Importer(ImporterSettnigs settings, ILogger<Importer> logger) {
_logger = logger;
this.settings = settings;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
_logger.LogInformation($"This is Key1: {settings.Key1}");
}
Now properly configure start up to use the provided configuration
public class Program {
public static async System.Threading.Tasks.Task Main(string[] args) {
var hostbuilder = new HostBuilder()
.UseWindowsService()
.ConfigureAppConfiguration((hostingContext, config) => {
var path = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location);
config
.SetBasePath(path)
.AddJsonFile("appsettings.json");
})
.ConfigureLogging(
options => options.AddFilter<EventLogLoggerProvider>(level =>
level >= LogLevel.Information)
)
.ConfigureServices((hostContext, services) => {
//get settings from app configuration.
ImporterSettings settings = hostContext.Configuration.Get<ImporterSettings>();
services
.AddSingleton(settings) //add to service collection
.AddHostedService<Importer>()
.Configure<EventLogSettings>(config => {
config.LogName = "Application";
config.SourceName = "Importer";
});
});
#if (DEBUG)
await hostbuilder.RunConsoleAsync();
#else
await hostbuilder..Build().RunAsync();
#endif
}
}
I have faced a situation when Task.Delay() method would trigger an event of cancellation in IApplicationLifetime. Here is the code:
static async Task Main(string[] args)
{
Console.WriteLine("Starting");
await BuildWebHost(args)
.RunAsync();
Console.WriteLine("Press any key to exit..");
Console.ReadKey();
}
private static IHost BuildWebHost(string[] args)
{
var hostBuilder = new HostBuilder()
.ConfigureHostConfiguration(config =>
{
config.AddEnvironmentVariables();
config.AddCommandLine(args);
})
.ConfigureAppConfiguration((hostContext, configApp) =>
{
configApp.SetBasePath(Directory.GetCurrentDirectory());
configApp.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<BrowserWorkerHostedService>();
services.AddHostedService<EmailWorkerHostedService>();
})
.UseConsoleLifetime();
return hostBuilder.Build();
}
and here are hosted services which are stopping abnormally:
public class BrowserWorkerHostedService : BackgroundService
{
private IApplicationLifetime _lifetime;
private IHost _host;
public BrowserWorkerHostedService(
IApplicationLifetime lifetime,
IHost host)
{
this._lifetime = lifetime;
this._host = host;
}
protected override async Task ExecuteAsync(CancellationToken stopToken)
{
while (!_lifetime.ApplicationStarted.IsCancellationRequested
&& !_lifetime.ApplicationStopping.IsCancellationRequested
&& !stopToken.IsCancellationRequested)
{
Console.WriteLine($"{nameof(BrowserWorkerHostedService)} is working. {DateTime.Now.ToString()}");
//lifetime.StopApplication();
//await StopAsync(stopToken);
await Task.Delay(1_000, stopToken);
}
Console.WriteLine($"End {nameof(BrowserWorkerHostedService)}");
await _host.StopAsync(stopToken);
}
}
public class EmailWorkerHostedService : BackgroundService
{
private IApplicationLifetime _lifetime;
private IHost _host = null;
public EmailWorkerHostedService(
IApplicationLifetime lifetime,
IHost host)
{
this._lifetime = lifetime;
this._host = host;
}
protected override async Task ExecuteAsync(CancellationToken stopToken)
{
while (!_lifetime.ApplicationStarted.IsCancellationRequested
&& !_lifetime.ApplicationStopping.IsCancellationRequested
&& !stopToken.IsCancellationRequested)
{
Console.WriteLine($"{nameof(EmailWorkerHostedService)} is working. {DateTime.Now.ToString()}");
await Task.Delay(1_000, stopToken);
}
Console.WriteLine($"End {nameof(EmailWorkerHostedService)}");
await _host.StopAsync(stopToken);
}
}
I would like my services to be running, unless lifetime.StopApplication() is triggered. However, hosted services are stopped, because lifetime.ApplicationStarted.IsCancellationRequested variable is set to true upon a second itteration.. Even though, in theory, I have no code that explicitly aborts the application.
Log will look like this:
Starting BrowserWorkerHostedService is working. 09.07.2019 17:03:53
EmailWorkerHostedService is working. 09.07.2019 17:03:53
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: xxxx
End EmailWorkerHostedService
End BrowserWorkerHostedService
Is there a good explanation why Task.Delay() triggers ApplicationStarted cancellation event?
You are misusing IApplicationLifetime events. Their purpose is to give you the ability to associate some action with them. For example you want to start message queue listening only when you application is fully started. You are going to do it like this:
_applicationLifetime.ApplicationStarted. Register(StartListenMq);
I think using CancellationTokens here wasn't the best idea, but it the way it was implemented.
When you want to cancel your HostedService you should check only token received in ExecuteAsync method. The flow will look look this:
IApplicationLifetime.StopApplication() => will trigger IApplicationLifetime.ApplicationStopping => will trigger IHostedService.StopAsync() => will stopToken
And now for your question: why it happens on await Task.Delay()? Look again at BackgroundService.StartAsync()
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
This code does't await ExecuteAsync. At the moment you call async operation in your code StartAsync() will continue to run. Somewhere in it's callstack it will trigger ApplicationStarted and since you are listening to it you will get _lifetime.ApplicationStarted.IsCancellationRequested = true.
I am encountering the following problem:
I have a ASP NET Core Application where I am using the following routes:
status, message, http.The first 2 accept a websocket request.
The problem is that the AppBuilder.Map in the pipeline does not work and it always sends me to the first route for all requests.
Program
class Program
{
static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)=>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel()
.UseSockets()
.UseUrls($"http://0.0.0.0:{Constants.SERVER_PORT}/")
.Build();
}
Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<StatusService>();//(x => new StatusService());
services.AddTransient<RegisterService>();
services.AddTransient<MessagingService>();
services.AddCors();
}
public void Configure(IApplicationBuilder builder)
{
builder.UseCors((p) => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials());
builder.UseWebSockets();
builder.Map("/status", app =>
{
builder.UseMiddleware<StatusWare>();
});
builder.Map("/http", app =>
{
builder.UseMiddleware<HTTPWare>();
});
builder.Map("/message", app =>
{
builder.UseMiddleware<MessageWare>();
});
}
}
The Middlewares all use their specific service which I will not post since the Invoke method of the other two middlewares does not get invoked.
Middlewares
Status
class StatusWare
{
StatusService handler;
public StatusWare(StatusService _handler,RequestDelegate del)
{
this.handler = _handler;
this.next = del;
}
RequestDelegate next;
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await this.next(context);
return;
}
await this.handler.AddClientAsync(context.WebSockets);
}
}
Message
class MessageWare
{
private MessagingService messageService;
private RequestDelegate next;
public MessageWare(MessagingService serv,RequestDelegate del)
{
this.messageService = serv;
this.next = del;
}
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await next(context);
}
await this.messageService.AcceptClientAsync(context.WebSockets);
}
}
HTTP
class HTTPWare
{
RequestDelegate next;
RegisterService registerService;
public HTTPWare(RequestDelegate _del,RegisterService service)
{
this.next = _del;
this.registerService = service;
}
public async Task Invoke(HttpContext context)
{
}
}
As you can see the middlewares are almost identical (I did not write anything in HttpWare since its Invoke method does not get called either.
So my question is .why despite using AppBuilder.Map all requests go into the first middleware StatusWare?
Could it be because of the way the specific services are added in ConfigureServices?
Configure is calling the UseMiddleware on the original builder and not on the delegate argument. In this case app
Update the Map calls to use the middleware on the builder delegate argument.
public void Configure(IApplicationBuilder builder) {
builder.UseCors((p) => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials());
builder.UseWebSockets();
builder.Map("/status", app => {
app.UseMiddleware<StatusWare>();
});
builder.Map("/http", app => {
app.UseMiddleware<HTTPWare>();
});
builder.Map("/message", app => {
app.UseMiddleware<MessageWare>();
});
}
Reference ASP.NET Core Middleware
I have troubles to get my ASP.NET Core SignalR app working.
I have this server-side code :
public class PopcornHub : Hub
{
private int Users;
public async Task BroadcastNumberOfUsers(int nbUser)
{
await Clients.All.InvokeAsync("OnUserConnected", nbUser);
}
public override async Task OnConnectedAsync()
{
Users++;
await BroadcastNumberOfUsers(Users);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
Users--;
await BroadcastNumberOfUsers(Users);
await base.OnDisconnectedAsync(exception);
}
}
whose SignalR Hub service is configured as :
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSignalR();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
app.UseSignalR(routes =>
{
routes.MapHub<PopcornHub>("popcorn");
});
...
}
In my client-side (WPF app), I have a service :
public class PopcornHubService : IPopcornHubService
{
private readonly HubConnection _connection;
public PopcornHubService()
{
_connection = new HubConnectionBuilder()
.WithUrl($"{Utils.Constants.PopcornApi.Replace("/api", "/popcorn")}")
.Build();
_connection.On<int>("OnUserConnected", (message) =>
{
});
}
public async Task Start()
{
await _connection.StartAsync();
}
}
My issue is that, when I call Start() method, I get the exception "Cannot start a connection that is not in the Initial state".
The issue occurs either locally or in Azure.
The SignalR endpoint is fine but no connection can be established. What am I missing?
You seem to be trying to start a connection that has already been started. Also note that as of alpha2 the connection is not restartable - i.e. if it is stopped you cannot restart it - you need to create a new connection.
EDIT
In post alpha2 versions the connection will be restartable.