Raul
Raul

Reputation: 3131

ASP.NET Core in a Worker Service doesn't call Configure IApplicationBuilder

I was migrating an ASP.NET Core App to the Worker Service template and was intending to keep the Startup code. However after

within Program I kept the CreateHostBuilder as described on the MS Docs:

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureWebHostDefaults(
            webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
        .ConfigureServices(
            services =>
            {
                services.AddHostedService<Worker>();
            });

while debugging, the ConfigureServices is being called

public void ConfigureServices(IServiceCollection services)

but the Configure within the Startup, is not reached / called

public void Configure(IApplicationBuilder app)

before it crashes calling Run().

I also tried this with the same result:

public static IHostBuilder CreateHostBuilder(string[] args)
    => Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(Startup.Configure))
        .ConfigureServices(
            services =>
            {
                Startup.ConfigureServices(services);
                services.AddHostedService<Worker>();
            });

The interesseting part is that the following code actually calls the Startup.Configure(IApplicationBuilder app):

public static IHostBuilder CreateHostBuilder(string[] args)
    => Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(Startup.Configure));

As soon as I am adding ConfigureServices it skips the IApplicationBuilder configuration call.

Am I missing something or what is the suggested way to achieve this?

Edit:

the exact error is:

InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Builder.IApplicationBuilder' while attempting to activate 'Kledex.Extensions.KledexAppBuilder'.

stack trace:

at Microsoft.Extensions.DependencyInjection.ServiceProvider..ctor(IEnumerable'1 serviceDescriptors, ServiceProviderOptions options) at Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(IServiceCollection services, ServiceProviderOptions options) at Microsoft.Extensions.DependencyInjection.DefaultServiceProviderFactory.CreateServiceProvider(IServiceCollection containerBuilder) at Microsoft.Extensions.Hosting.Internal.ServiceFactoryAdapter'1.CreateServiceProvider(Object containerBuilder) at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider() at Microsoft.Extensions.Hosting.HostBuilder.Build()

the error happens as soon as it reaches CreateHostBuilder(args).Build().Run(); and tries to resolve the registered services, while the above one has a dependency to some config app.UseSomething(); within the Startup.Configure() method.

A breakpoint in Startup.Configure() doesn't get hit.

Upvotes: 11

Views: 8090

Answers (5)

Marcelo Vega
Marcelo Vega

Reputation: 1

I get the following configuration. I'm using .net Core 9 (but it also works on .net core 8), the first thing is to load the following packages:

<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.AppSettings" Version="3.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />

Then the Worker Service configuration is done as follows:

    var builder = Host.CreateApplicationBuilder(args);
    
    var logger = new LoggerConfiguration()
        .ReadFrom.Configuration(builder.Configuration)
        .Enrich.FromLogContext()
        .CreateLogger();
    
    Log.Logger = logger;
    
    Log.Information("Starting host");
    Log.Information($"Evironment [{builder.Environment.EnvironmentName}]");
    
builder.Services.AddSerilog(logger);

builder.Services.AddCronJob<ScopedProcessingSignService>(options =>
{
    options.CronExpression = "* * * * *";
    options.TimeZone = TimeZoneInfo.Local;
});

var host = builder.Build();

host.Run();

when doing a var builder = Host.CreateApplicationBuilder(args); passing it as a parameter to .ReadFrom.Configuration(builder.Configuration), the Serilog configuration takes it without problems.

Important point, you must pass the newly generated logger as parameter to builder.Services.AddSerilog(logger);

Finally the configuration in the appsettings is the following:

},
"Serilog": {
  "Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Console" ],
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "Microsoft": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning",
      "System": "Information"
    }
  },
  "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
  "WriteTo": [
    {
      "Name": "Console",
      "Args": {
        "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
        "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}"
      }
    },
    {
      "Name": "File",
      "Args": {
        "path": "C:\\Logs\\Sign\\Sign-.log",
        "rollingInterval": "Day",
        "retainedFileCountLimit": 14,
        "shared": true,
        "buffered": false,
        "flushToDiskInterval": "00:00:10",
        "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}"
      }
    }
  ]
}

Upvotes: 0

Andy
Andy

Reputation: 13527

Here's your problem:

You are trying to use a pattern that was specifically designed for ASP.NET Core, but you aren't using ASP.NET Core and because of that you aren't getting the Startup pattern or any of it's features such as IApplicationBuilder or IWebHostEnvironment.

A while back, someone asked if we could figure out how to get the Startup pattern of ASP.NET Core in to something that uses the generic host builder without the use of ASP.NET Core. I came up with an answer.

The above answer doesn't give you the Configure(IApplicationBuilder app, IWebHostEnvironment env) method, and it shouldn't. That method is specifically for a Web Host. Not a worker. There is no reason you should need this method unless you are starting up a Web Host.

And if that's the case, then you should start over and create a brand new ASP.NET Core application... Not a Worker. This will configure your application to use Microsoft.NET.Sdk.Web SDK and not Microsoft.NET.Sdk.Worker SDK giving you the features you lost by going the Worker route.

You will not get an answer that will work for you unless you move away from the Worker SDK and move to the ASP.NET Core SDK. Maybe someone will come up with something, but more than likely it will be a kludge. And kludges break overtime and are a PITA to maintain.

There is nothing wrong with using Microsoft.NET.Sdk.Web for a Windows service. Microsoft even shows you how (Specifically pay attention to Deployment Type section).

Upvotes: 8

nunohpinheiro
nunohpinheiro

Reputation: 2269

Given that you are trying to configure web host defaults and use ASP.NET Core features like IApplicationBuilder, I believe that you are planning to run the service over some kind of web server. Is this correct?

If so, besides the other good suggestions, I have another one, which is to keep using an ASP.NET Core app - obviously enabling the usage of the Startup almost as is (with ConfigureServices and Configure methods) and perhaps most of your previous app.

To do so, in your Worker csproj, you could replace <Project Sdk="Microsoft.NET.Sdk.Worker"> by <Project Sdk="Microsoft.NET.Sdk.Web">.

Then, you can configure your CreateHostBuilder such as:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(
            webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Please note that I removed UseWindowsService() on purpose, since I am describing a case of using Worker services and not specifically Windows services.

Now you can keep using your Startup class, adding the registration of your background service(s):

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        // ...
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddHostedService<Worker>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
    }
}

Hope this helps :) However, if you specifically want to develop Windows services rather than Worker services, please let us know.

In this case, we keep having an ASP.NET Core WebApp/WebAPI with all its capabilities. We just added background services to it :)

Upvotes: 5

Alexander I.
Alexander I.

Reputation: 2714

Yes, I had the same issue.

Answer for the question we can find in the Microsoft documentation here.

We need to use method ConfigureAppConfiguration for Host Services.

Please review my code examples. We use this code and it works fine:


Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureAppConfiguration((context, builder) =>
            {
                var environment = context.HostingEnvironment.EnvironmentName;
                builder.AddAppSettings(environment);
            })
            .ConfigureWebHostDefaults(builder =>
            {
                builder.UseStartup<Startup>();
            });
}

HostedService

public class HostedService : IHostedService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    private readonly ILogger<HostedService> _logger;

    public HostedService(IServiceScopeFactory serviceScopeFactory, ILogger<HostedService> logger)
    {
        _serviceScopeFactory = serviceScopeFactory;
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Service is starting...");

        // Run new task and use your business logic
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            // Resolve business manager class with many dependencies. It is Scoped LifeTime.
            var manager = scope.ServiceProvider.GetRequiredService<IClientManager>();
        }

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Service is stopping...");
        return Task.CompletedTask;
    }
}

enter image description here


So we had only one trouble with this approach, we can inject only singletons to HostedService class. We will inject IServiceScopeFactory to constructor. And after it we create scope, resolve all needed dependencies and run our business logic.

Upvotes: 2

You can move configuring of your HostedService from Program class to Startup. Just add this to ConfigureServices method before calling AddControllers:

services.AddHostedService<Worker>();

I'm including HostedService in my project in this way and it works.

That's my standart CreateHostBuilder:

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });

Upvotes: -3

Related Questions