Reputation: 9975
I currently have a dotnet 5.0 Worker Service project which hosts a background service that executes a line-of-business process at regular intervals. I've managed to fumble my way through setting up a host inside a CreateHostBuilder
method, and this is all running fine as an executable inside a docker container.
However, I would now like to add a HTTP api / ui so that I can expose a "health" endpoint and some pretty web pages that access the same in-process memory as the background service to do things like displaying the contents of an in-memory cache, evicting cache entries, manually triggering a run of the business process etc, and I'm completely lost.
I'm thinking I just somehow configure an additional hosted service in the ConfigureServices
method that binds to a class defined in an ASPNET razor project on ports I specify, but after several attempts I'm a bit stuck with how to start approaching this, let alone make it actually work :-(.
Specifically, I have a C# project that looks like this:
MyWorker.csproj
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.0.0-dev-00051" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.1-dev-00940" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00839" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyAppCode\MyAppCode.csproj" />
</ItemGroup>
</Project>
and a Program.cs that looks like this:
public static void Main(string[] args)
{
Program.InitSerilog();
Program.CreateHostBuilder(args).Build().Run();
}
public static void InitSerilog()
{
... do stuff ...
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return new HostBuilder()
// worker service config
.ConfigureLogging((context, builder) =>
{
builder.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureServices(services =>
{
services.AddHostedService<MyAppService>();
services.AddSingleton<AppSettings>(
serviceProvider => { return Program.CreateAppSettings(); }
);
})
.UseSerilog();
}
private static AppSettings CreateAppSettings()
{
... bind to environment variables and return ...
}
}
Finally, my existing background service looks like this:
public sealed class MyAppService: BackgroundService
{
public MyAppService(ILogger<MyAppService> logger, AppSettings appSettings)
: base()
{
... do stuff ...
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
... keep doing the main business process in a timed loop ...
}
#endregion
}
I've found a lot of examples of how to add a worker service to an existing ASPNET project, or how to host an ASPNET project as a Windows Service, but nothing on how to add an ASPNET site listener to an existing Worker Service.
What I'd like to be able to do is build the web ui / api in a new c# project and somehow just plumb it in to the existing Program.cs so it listens on a specified port while the main BackgroundService is running, but maybe I'm just approaching this all wrong?
Note - it's possible I undid some of the default "just works" magic in the CreateHostBuilder
method while I was tinkering, so feel free to tell me to put that back to the default if that's the easy answer :-).
Any help / pointers appreciated...
Upvotes: 3
Views: 3664
Reputation: 2269
Firstly, I would suggest you turned your <Project Sdk="Microsoft.NET.Sdk.Worker">
into <Project Sdk="Microsoft.NET.Sdk.Web">
.
Then, change your CreateHostBuilder
to use a Startup
class:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return new HostBuilder()
.ConfigureLogging((context, builder) =>
{
builder.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog();
}
And in your Startup
, you can handle both the background and other services' configuration:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddAuthorization();
// worker service config
services.AddHostedService<MyAppService>();
services.AddSingleton<AppSettings>(
serviceProvider => { return Program.CreateAppSettings(); } // this method could be relocated too
);
// example for health checks
services.AddHealthChecks();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// example for health checks
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
});
}
}
Upvotes: 6