ASP.NET Core 2.1 introduced the ASP.NET Core Generic Host for non-HTTP scenarios. In standard HTTP ASP.NET Core applications, you configure your app using the WebHostBuilder in ASP.NET Core, but for non-HTTP scenarios (e.g. messaging apps, background tasks) you use the generic HostBuilder.

In my previous post, I discussed some of the similarities and differences between the IWebHostBuilder and IHostBuilder. In this post I introduce the Serilog.Extensions.Hosting package for ASP.NET Core generic hosts, discuss why it's necessary, and describe how you can use it to add logging with Serilog to your non-HTTP aps.

Why do you need Serilog.Extensions.Hosting?

The goal of the ASP.NET Core generic host is to provide the same cross-cutting capabilities found in traditional ASP.NET Core apps, such as configuration, dependency injection (DI), and logging, to non-HTTP apps. However, it does so while also using a whole new set of abstractions, as discussed in my previous post. If you haven't already, I suggest reading that post for a description of the problem.

Serilog already has a good story for adding logging to your traditional HTTP ASP.NET Core apps with the Serilog.AspNetCore library, as well as an extensive list of available sinks. Unfortunately, the abstraction incompatibilities mean that you can't use this library with generic host ASP.NET Core apps.

Instead, you should use the Serilog.Extensions.Hosting library. This is very similar to the Serilog.AspNetCore library, but designed for the Microsoft.Extensions.Hosting abstractions instead of the Microsoft.AspNetCore.Hosting abstractions (Extensions instead of AspNetCore).

In this post I'll give a quick example of how to use the library, and touch on how it works under the hood. Alternatively, check out the code and Readme on GitHub.

Adding Serilog to a generic Host

The Serilog.Extensions.Hosting package contains a custom ILoggerFactory that uses the standard Microsoft.Extensions.Logging infrastructure to log to Serilog. Any messages that are logged using the standard ILogger interface are sent to Serilog. That includes both custom log messages and infrastructure messages, as you'd expect.

If you're new to Serilog, I suggest seeing their website. I've also written about Serilog previously.

Installing the library

You can install the Serilog.Extensions.Hosting NuGet package into your app using the package manager console, the .NET CLI, or by simply editing your app's .csproj file. You'll also need to add at least one "sink" - this is where Serilog will write the log messages. Serilog.Sinks.Console writes messages to the console for example.

Using the package manager:

Install-Package Serilog.Extensions.Hosting -DependencyVersion Highest  
Install-Package Serilog.Sinks.Console  

Using the .NET CLI:

dotnet add package Serilog.Extensions.Hosting  
dotnet add package Serilog.Sinks.Console  

This will install the packages in your app, as you can see from the .csproj file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Serilog.Sinks.Console" Version="3.0.1" />
    <PackageReference Include="Serilog.Extensions.Hosting" Version="2.0.0" />
  </ItemGroup>

</Project>  

Configuring Serilog in your application

Once you've restored the packages (either automatically or by running dotnet restore), you can configure your app to use Serilog. The recommended approach is to configure Serilog's static Log.Logger object first, before configuring your ASP.NET Core application. That way you can use a try/catch block to ensure any start-up issues with your app are appropriately logged.

In the following example, I manually configure Serilog to only log Information level or higher events. Additionally, only events in the Microsoft namespace of Warning or above will be logged.

You can load the Serilog configuration using IConfiguration objects instead using the Serilog configuration library.

public class Program  
{
    public static int Main(string[] args)
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Information()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .CreateLogger();

        try
        {
            Log.Information("Starting host");
            CreateHostBuilder(args).Build().Run();
            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args); //see below
}

Finally, add the Serilog ILoggerFactory to your IHostBuilder with UserSerilog() method in CreateHostBuilder():

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        new HostBuilder()
            .ConfigureHostConfiguration(builder => { /* Host configuration */ })
            .ConfigureAppConfiguration(builder => { /* App configuration */ })
            .ConfigureServices(services =>  { /* Service configuration */})
            .UseSerilog(); // <- Add this line
}

That's it! When you run your application You'll see log output something like the following (depending on the services you have configured!):

[22:10:39 INF] Starting host
[22:10:39 INF] The current time is: 12/05/2018 10:10:39 +00:00

How the library works behind the scenes

On the face of it, completely replacing the default ASP.NET Core logging system to use a different one seems like a big deal. Luckily, thanks to the use of interfaces, loose coupling, and dependency injection, the code is remarkably simple! The whole extension method we used previously is shown below:

public static class SerilogHostBuilderExtensions  
{
    public static IHostBuilder UseSerilog(this IHostBuilder builder, 
        Serilog.ILogger logger = null, bool dispose = false)
    {
        builder.ConfigureServices((context, collection) =>
            collection.AddSingleton<ILoggerFactory>(services => new SerilogLoggerFactory(logger, dispose)));
        return builder;
    }
}

The UseSerilog() extension calls the ConfigureServices method on the IHostBuilder, and adds an instance of the SerilogLoggerFactory as the application's ILoggerFactory. Whenever an ILoggerFactory is required by the app (to create an ILogger), the SerilogLoggerFactory will be used.

The SerilogLoggerFactory is essentially a simple wrapper around the SerilogLoggerProvider provided in the Serilog.Extensions.Logging library. This library implements the necessary adapters so Serilog can hook into the APIs required by the Microsoft.Extensions.Logging framework.

The framework's default LoggerFactory implementation allows multiple providers to be active at once (e.g. a Console provider, a Debug provider, a File provider etc). In contrast, the SerilogLoggerFactory allows only the Serilog provider, and ignores all others.

public class SerilogLoggerFactory : ILoggerFactory  
{
    private readonly SerilogLoggerProvider _provider;

    public SerilogLoggerFactory(ILogger logger = null, bool dispose = false)
    {
        _provider = new SerilogLoggerProvider(logger, dispose);
    }

    public void Dispose() => _provider.Dispose();

    public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
    {
        return _provider.CreateLogger(categoryName);
    }

    public void AddProvider(ILoggerProvider provider)
    {
        // Only Serilog provider is allowed!
        SelfLog.WriteLine("Ignoring added logger provider {0}", provider);
    }
}

And that's it! The whole library only requires two .cs files, but it makes adding Serilog to an ASP.NET Core generic host that little bit easier. I hope you'll give it a try, and obviously raise an issues you find or comments you have on GitHub!

Summary

ASP.NET Core 2.1 introduced the generic host, for handling non-HTTP scenarios, like messaging or background tasks. The generic host uses the same underlying abstractions for configuration, dependency injection, and logging, but the main interfaces exist in a different namespace: Microsoft.Extensions.Hosting instead of Microsoft.AspNetCore.Hosting.

This difference in namespace means you need to use the Serilog.Extensions.Hosting NuGet package to add Serilog logging to your generic host app (instead of Serilog.AspNetCore). After adding the package to your app, configure your static Serilog logger as usual and call UserSerilog() on your IHostBuilder instance.