mclayton
mclayton

Reputation: 9975

Adding an ASPNET API to an existing Worker Service project

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

Answers (1)

nunohpinheiro
nunohpinheiro

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

Related Questions