I have a console program with a simple consuming backgroundservice which need to call function from a scopedservice that uses a HttpClientFactory to call an external API and return result to consuming backgroundservice.
I want very simple after looking at a couple example online to remove all possible complexity from the code.
public class Program
{
public static async Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureLogging((hostContext, config) =>
{
config.AddConsole();
})
.ConfigureAppConfiguration((hostContext, config) =>
{
config.AddJsonFile("appsettings.json", optional: true);
config.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true);
config.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ConsumeMessageService>();
services.AddScoped<IScopedArcGisServices, ScopedArcGisServices>();
})
.UseConsoleLifetime()
.Build();
using (host)
{
// Start the host
await host.StartAsync();
// Wait for the host to shutdown
await host.WaitForShutdownAsync();
}
}
}
public class ConsumeMessageService : IHostedService
{
private readonly ILogger _logger;
public IServiceProvider _serviceProvider { get; }
public ConsumeMessageService(IServiceProvider services,
ILogger<ConsumeMessageService> logger)
{
_serviceProvider = services;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation(
"Consume message service hosted service is starting.");
DoWork();
return Task.CompletedTask;
}
private void DoWork()
{
_logger.LogInformation(
"Consume message service hosted service is working.");
using (var scope = _serviceProvider.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedServices>();
scopedProcessingService.DoWork();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
return Task.CompletedTask;
}
}
internal interface IScopedServices
{
void DoWork();
}
internal class ScopedServices : IScopedServices
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _clientFactory;
public string JsonResult { get; private set; }
public ScopedServices(ILogger<ScopedServices> logger, IHttpClientFactory clientFactory)
{
_logger = logger;
_clientFactory = clientFactory;
}
public void DoWork()
{
_logger.LogInformation("Scoped processing service is working.");
}
}
As soon as I had the scopedservice with the HttpClientFactory I get this message :
Unable to resolve service for type
'System.Net.Http.IHttpClientFactory' while attempting to activate
'Integration.BackgroundService.Services.ScopedServices'.'
IHttpClientFactory is not added by default.
You have to call services.AddHttpClient() when configuring services to add access to the factory and its related clients.
//...
.ConfigureServices((hostContext, services) => {
services.AddHttpClient(); //<-- THIS IS NEEDED
services.AddHostedService<ConsumeMessageService>();
services.AddScoped<IScopedArcGisServices, ScopedArcGisServices>();
})
//...
For more on how to configure the clients created by the factory,
Reference Use HttpClientFactory to implement resilient HTTP requests
Related
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: WindowServiceSample1.Worker': Cannot consume scoped service 'WindowServiceSample1.Data.ApplicationDbContext' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)
using WindowServiceSample1;
using WindowServiceSample1.Data;
using Microsoft.EntityFrameworkCore;
IHost host = Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "Abc Service New";
})
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer("Server=DESKTOP-4NUBQP8\\SQLEXPRESS;Database=MyDatabase;Trusted_Connection=True;"));
})
.Build();
await host.RunAsync();
Worker class code -
namespace WindowServiceSample1
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly ApplicationDbContext _db;
public Worker(ILogger<Worker> logger, ApplicationDbContext db)
{
_logger = logger;
_db = db;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Notification item = new Notification();
item.EUID = "test";
item.StoreNumber = "test";
item.NotificationType = "test";
item.Status = "test";
_db.Notifications.Add(item);
await _db.SaveChangesAsync();
_logger.LogWarning("ABC Service running at: {time}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
}
Application DB Context class
namespace WindowServiceSample1.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Notification> Notifications { get; set; }
}
}
As the error shows we should consume scope services in scoped instances to avoid data corruption or threading issue.
You should use Using ServiceScopeFactory to resolve scope instances.
Like this:
public Worker(ILogger<Worker> logger, , IServiceScopeFactory factory )
{
_logger = logger;
_db = factory.CreateScope().ServiceProvider.GetRequiredService<ApplicationDbContext>();
}
}
I'm having a look at MassTransit, and I used the masstransit dotnet temaplate to generate a worker, as per https://masstransit-project.com/getting-started/ ( everything up till RabbitMQ )
Then I was interested in getting the built in mediator working with responses, so changed the code according to https://masstransit-project.com/articles/mediator.html
so it the setup looks like ...
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMediator(x =>
{
x.AddConsumer<MessageConsumer>();
x.AddRequestClient<Message>();
});
services.AddMassTransit(x =>
{
x.AddConsumersFromNamespaceContaining<MessageConsumer>();
x.UsingInMemory((context,cfg) =>
{
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService(true);
services.AddHostedService<Worker>();
});
and the consumer / contract now looks like
public class Message { public string Text { get; set; } }
public class MessageResult { public string Text { get; set; } }
public class MessageConsumer : IConsumer<Message>
{
readonly ILogger<MessageConsumer> _logger;
public MessageConsumer(ILogger<MessageConsumer> logger)
{
_logger = logger;
}
public Task Consume(ConsumeContext<Message> context)
{
_logger.LogInformation("Received Text: {Text}", context.Message.Text);
return context.RespondAsync(new MessageResult() {Text = $"Got {context.Message.Text}"});
}
}
and the worker looks like
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IBus _bus;
private readonly IRequestClient<Message> _request;
public Worker(ILogger<Worker> logger, IBus bus, IRequestClient<Message> request)
{
_logger = logger;
_bus = bus;
_request = request;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var response = await _request.GetResponse<MessageResult>(new Message {Text = $"The time is {DateTimeOffset.Now}"}, stoppingToken);
await Task.Delay(1000, stoppingToken);
}
}
}
however when I run it, the injection of IRequestClient seems to fail ( for some reason it wasn't registered? ) with the exception
Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: MTGettingStarted.Worker': Cannot consume scoped service 'MassTransit.IRequestClient`1[MTGettingStarted.Message]' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.
Which is what I thought x.AddRequestClient<Message>(); should have done. Maybe the documentation is incomplete? or I missed something?
If I change the worker to manaully get the request, then it does work
var client = _mediator.CreateRequestClient<Message>();
var response = await client.GetResponse<MessageResult>(new Message {Text = $"The time is {DateTimeOffset.Now}"}, stoppingToken);
But still curious why the DI doesn't work?
IRequestClient<T> is registered in the container as scoped, which the error you posted indicates:
Cannot consume scoped service
Your hosted service is a singleton.
To get the sample working you have to enable your worker to use scoped services.
Take a look at the docs at: https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service
In the implementation behind IScopedProcessingService you have to inject IRequestClient<>.
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
using (var scope = _serviceProvider.CreateScope())
{
var scopedProcessingService = scope.ServiceProvider.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
I know a IHostedService that runs only one time sounds like a console application, but the reason I want to use it instead of a plain console application is:
.net core introduces Generic Host for running non-http application
A plain console application does not have DI, Logger, Configurations ready to use
By using the following code, I'm able to somewhat achieve this one-time behaviour, however, I could not find a way to gracefully exit the app after it finishes.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) => { services.AddHostedService<StartUp>(); });
}
Where StartUp is a simple IHostedService
public class StartUp:IHostedService
{
private ILogger<StartUp> _logger;
public StartUp(ILogger<StartUp> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("start async");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("stop async");
return Task.CompletedTask;
}
}
How can I stop the app gracefully?
Or if this is completely wrong, how should I implement this one-time application?
Yes, you can achieve that by injecting IHostApplicationLifetime into your hosted service.
Example:
public class StartUp:IHostedService
{
private readonly IHostApplicationLifetime _host;
private ILogger<StartUp> _logger;
public StartUp(IHostApplicationLifetime host, ILogger<StartUp> logger)
{
_host = host;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("start async");
_host.StopApplication();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("stop async");
return Task.CompletedTask;
}
}
You can also set exit code by setting Environment.ExitCode.For example:
Environment.ExitCode = 0;
_host.StopApplication();
If you are just using IHostedService as a workaround for the missing DI and ILogger you can also setup DI with the ILogger and IConfiguration directly without IHostedService
public class Program
{
public static async Task Main(string[] args)
{
var configBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true);
var config = configBuilder.Build();
var sp = new ServiceCollection()
.AddLogging(b => b.AddConsole())
.AddSingleton<IConfiguration>(config)
.AddSingleton<IFooService, FooService>()
.BuildServiceProvider();
var logger = sp.GetService<ILoggerFactory>().CreateLogger<Program>();
logger.LogDebug("Starting");
var bar = sp.GetService<IFooService>();
await bar.DoAsync();
}
}
With this setup your code is just running once, can resolve every service you register in the ServiceCollection and no need of a Host to start
Example: .netfiddle
You have to call IHostApplicationLifetime.StopApplication() for the application to close. You can inject IHostApplicationLifetime and call StopApplication() when your done.
Another implementation might be using IHostBuilder, but without calling run method.
static Task Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
var manager = host.Services.GetRequiredService<IManager>();
manager.DoSomething();
// we don't have registered hosted services therefore we don't call host.RunAsync();
return Task.CompletedTask; // or change signature to void and remove return
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
services
.AddScoped<IFoo, Foo>()
.AddScoped<IBar, Bar>()
.AddSingleton<IManager, Manager>());
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 am creating console application in asp.net core which is going to run as background service in different environments. I have used "BackgroundService" class provided by "Microsoft.Extensions.Hosting". I want to run its "ExecuteAsync" method when my program gets started.
File: Program.cs
public static void Main(string[] args)
{
var host = new HostBuilder()
.ConfigureHostConfiguration(configHost =>
{
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<IHostedService,RabbitLister>();
})
.UseConsoleLifetime()
.Build();
}
File: RabbitLister.cs
public class RabbitLister : BackgroundService
{
private readonly IEventBus _eventBus;
private readonly ILogger<RabbitLister> _logger;
public RabbitLister()
{
}
public RabbitLister(IEventBus eventBus, ILogger<RabbitLister> logger)
{
_eventBus = eventBus;
_logger = logger;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_eventBus.SubscribeDynamic("myQueue");
return Task.CompletedTask;
}
}
After the host has been build, invoke host.Run()
public static void Main(string[] args) {
var host = new HostBuilder()
.ConfigureHostConfiguration(configHost => {
})
.ConfigureServices((hostContext, services) => {
services.AddHostedService<IHostedService, RabbitLister>();
})
.UseConsoleLifetime()
.Build();
//run the host
host.Run();
}
that will start hosted service and eventually call the execute function