Running Kestrel together with generic host

 

~ 3 min read.

Sometimes you will be in situation where you would need to run a web server and generic host or a gRPC server.

Net Core 2.2

To achieve that you would need to register an instance of IHost and also an instance of IWebHost. Sounds simple, it pretty much is, but you will also need to run them both, which puts you in a bit of peculiar situation, or just an ugly looking one.

So, you code might look something like this:

internal static Task Main(string[] args)
{
	var source = new CancellationTokenSource();

	IWebHost webHost = new WebHostBuilder()
		.UseContentRoot(Directory.GetCurrentDirectory())
        .UseConfiguration(
            new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("hostsettings.json")
                .Build())
        .UseKestrel((ctx, ops) =>
        {
            var ip = ctx.Configuration.GetSection("KestrelOptions").GetValue<string>("Ip");
            var port = ctx.Configuration.GetSection("KestrelOptions").GetValue<int>("Port");

            ops.Listen(IPAddress.Parse(ip), port);
        })
        .ConfigureServices((ctx, services) =>
        {
            services.AddLogging();
			services.AddTransient<IMyService, MyService>();
        })
        .UseStartup<Startup>()
        .ConfigureServices(configure)
        .Build();

    IHost host = new HostBuilder()
        .ConfigureHostConfiguration(configHost =>
        {
            configHost.SetBasePath(Directory.GetCurrentDirectory());
            configHost.AddJsonFile("hostsettings.json");
        })
        .ConfigureServices((hostContext, configSvc) =>
        {
            configSvc.AddLogging();
            configSvc.AddHostedService<ConsoleHostedService>();
        })
        .UseConsoleLifetime()
        .Build();

    await host.StartAsync(source.Token);

	webHost.RunAsync(source.Token);

	await host.WaitForShutdownAsync(source.Token);
}

Not that terrible, and not that great either.

Net Core 3.0

Thankfully in net core 3.0 this conundrum is resolved, and all hosts are unified under one interface IHost, so when we want to register an instance of Kestrel (WebHost) and an instance of generic host we will be able to use only IHost interface which will simplify our code quite a bit and also our DI setup.

public static Task Main(string[] args)
{
    IHost builder =
        Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(builder =>
        {
            builder.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");
            builder.UseStartup<Startup>();

            builder.ConfigureServices(x => x.AddTransient<IMyService, MyService>());
        })
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<ConsoleHostedService>();
        })
        .Build();

    return builder.RunAsync();
}

In case you wonder how did they unify the IHost and IWebHost behold the magic behind the extension method: ConfigureWebHostDefaults:

public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
    return builder.ConfigureWebHost(webHostBuilder =>
    {
        WebHost.ConfigureWebDefaults(webHostBuilder);

        configure(webHostBuilder);
    });
}

As you can see IWebHost still exist which was expected since this was not a trivial task to implement, but the important note is that you don’t have to register your types twice if you need them for web host and generic host to be more clear imagine that your web host has a dependency to IMyService interface the below DI setup will work:

public static Task Main(string[] args)
{
    IHost builder =
        Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(builder =>
        {
            builder.UseSetting(WebHostDefaults.DetailedErrorsKey, "true");
            builder.UseStartup<Startup>();

            builder.ConfigureServices(x => x.AddTransient<IMyService, MyService>());
        })
        .ConfigureServices((hostContext, services) =>
        {
			services.AddTransient<IMyService, MyService>();
            services.AddHostedService<ConsoleHostedService>();
        })
        .Build();

    return builder.RunAsync();
}

The IServiceCollection instance is shared between two hosts, so you could’ve register the IMyService interface in ConfigureWebHostDefaults if you wanted.

Conclusion

This might be an edge case that lots of people will not have a need to use, but definite case where you will have a need to run a web server and a hosted service is running a gRPC server and exposing the health check endpoints via HTTP which would be a requirement when you are running your gRPC app in Kubernetes cluster. In order to configure K8s’s liveness and readiness probes you need to have HTTP endpoints exposed and since gRPC is using HTTP/2 protocol you cannot expose them as gRPC services.