Serilog 5.0.0 バージョンアップ時に引っかかった2点

# はじめに

Serilog をバージョンアップ(v4.1.0 ⇒ v5.0.0)したら警告と実行時エラーに遭遇したので、対処方法をメモしておきます。

# IWebHostBuilder に対する AddSerilog が非推奨になった

IWebHostBuilder は ASP.NET Core 2.1 でアプリケーションの初期化に利用されていたインターフェイスで、ASP.NET Core 3.1 以降では IHostBuilder に置き換えることが推奨されています。
Serilog 5.0.0 では、IWebHostBuilder に対する AddSerilog メソッドが非推奨としてマークされているので、ASP.NET 2.1 から 3.1 への移行時にIWebHostBuilder のまま移行していた場合、Serilog を 5.0.0 以上にバージョンアップすると下記の警告が表示されます。

```
警告 CS0618 'SerilogWebHostBuilderExtensions.UseSerilog(IWebHostBuilder, Action<WebHostBuilderContext, LoggerConfiguration>, bool, bool)' は旧形式です ('Prefer UseSerilog() on IHostBuilder')
```

例えば次のような ASP.NET Core 5 までの初期化コードの場合、

```cs:Program.cs
    public class Program
    {
        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .WriteTo.Console()
                .CreateBootstrapLogger();
            try
            {
                CreateHostBuilder(args).Build().Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Unhandled exception");
            }
            finally
            {
                Log.Information("Shut down complete");
                Log.CloseAndFlush();
            }
        }

        public static IWebHostBuilder CreateHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseSerilog((context, configuration) =>
                    configuration.ReadFrom.Configuration(context.Configuration));
                .UseStartup<Startup>();
    }
```

下記のような IHostBuilder を使った形に書き換えてあげればよいですね。

```cs:Program.cs
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .UseSerilog((context, configuration) =>
                    configuration.ReadFrom.Configuration(context.Configuration));
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
```

もしくは、.NET 6.0 の Mimimal API の形に書き換えてもよいです。

```cs:Program.cs
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();
try
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.UseSerilog((context, configuration) =>
        configuration.ReadFrom.Configuration(context.Configuration));

    // ... 略 ...

    var app = builder.Build();

    // ... 略 ...

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Unhandled exception");
}
finally
{
    Log.Information("Shut down complete");
    Log.CloseAndFlush();
}
```

# IWebHostBuilder を自前で BuildServiceProvider すると例外が発生する

以前から Startup 時に自前で IServiceCollection をビルドすると、シングルトンが複数生成されるなどしておかしくなるので非推奨でしたが、Serilog 5.0.0 以上を利用している場合、ServiceCollection をビルドしたタイミングで実行時エラーが発生します。

```
System.InvalidOperationException: The logger is already frozen.
   at Serilog.Extensions.Hosting.ReloadableLogger.Freeze()
   at Serilog.SerilogHostBuilderExtensions.<>c__DisplayClass3_1.<UseSerilog>b__1(IServiceProvider services)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)

   ... 略 ...

   at Microsoft.Extensions.Hosting.HostBuilder.Build()
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
   at Program.<Main>$(String[] args) in C:\Users\YoichiSugiyama\source\repos\WebApplication1\WebApplication2\Program.cs:line 18
```

初期化中に DI コンテナの中身が欲しくなる多くのケースは Scope か Singleton なサービスの初期化だと思うので、次のように書き換えます。

例えば、DbContext に独自の LoggerFactory を渡したい場合は、サービスコンテナをビルドしてサービスを取り出すのではなく、初期化時にサービスコンテナを受け取りサービスを取り出します。

```cs:変更前
    var factory = builder.Services.BuildServiceProvider().GetRequiredService<ILoggerFactory>();
    builder.Services.AddDbContext<MyDbContext>(options =>
    {
        options.UseLoggerFactory(factory);
        options.UseMySql(builder.Configuration.GetConnectionString("myDb"), ServerVersion.Parse("5.7"));
    });
```

```cs:変更後
    builder.Services.AddDbContext<MyDbContext>((services, options) =>
    {
        options.UseLoggerFactory(services.GetRequiredService<ILoggerFactory>());
        options.UseMySql(builder.Configuration.GetConnectionString("myDb"), ServerVersion.Parse("5.7"));
    });
```

シングルトンなサービスの初期化も同様ですね。

```cs:変更前
    builder.Services.AddSingleton<DbSettingValueProxy>();
    var proxy = builder.Services.BuildServiceProvider().GetRequiredService<DbSettingValueProxy>();
    builder.Services.AddSingleton(new HogeService(new HogeServiceOptions
    {
        Option1 = proxy.GetValue("prop1")
    }));
```

```cs:変更後
    builder.Services.AddSingleton<DbSettingValueProxy>();
    builder.Services.AddSingleton(services =>
    {
        var proxy = services.GetRequiredService<DbSettingValueProxy>();
        return new HogeService(new HogeServiceOptions
        {
            Option1 = proxy.GetValue("prop1")
        });
    });
```

サービス側が Options パターンを使っている場合は、Configure 時に型パラメーターに取得用のサービスを指定すれば DI で解決することができます。

```cs:変更前
    builder.Services
        .AddOptions<HogeServiceOptions>()
        .Configure(options =>
        {
            var proxy = builder.Services.BuildServiceProvider().GetRequiredService<DbSettingValueProxy>();
            options.Option1 = proxy.GetValue("prop1");
        });
```

```cs:変更後
    builder.Services
        .AddOptions<HogeServiceOptions>()
        .Configure<DbSettingValueProxy>((options, proxy) =>
        {
            options.Option1 = proxy.GetValue("prop1");
        });
```

# まとめ

行儀が悪い事をしていると、ある日困ったことになります。