🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

.NET CoreのGeneric Host(汎用ホスト)でEntityFramework Coreを使用する際にService生成に失敗する件の解決方法

きじのしっぽさん作成の逆引きEntity Framework Core 3.1ハンドブックが届いたのでEntityFramework Coreに再入門してみました。

booth.pm

その過程でちょっと見慣れないエラーに遭遇したので、記録しておきます。
環境は.NET Core 3.1 LTSのMicrosoft.EntityFrameworkCore.InMemory Version 3.1.0です。

目次

再現手順

下記コマンドでGeneric Host(汎用ホスト)なテンプレートを作ります。

dotnet new worker -o EFCore.Sample
cd EFCore.Sample
dotnet add package Microsoft.EntityFrameworkCore.InMemory

以下のコードを書きました。

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

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    var con = hostContext.Configuration.GetConnectionString("DbName");
                    services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase(con));
                    services.AddSingleton<IRepository, TodoRepository>();
                    services.AddHostedService<Worker>();
                });
}
public interface IRepository { }
public class TodoRepository : IRepository
{
    private readonly TodoContext _db;
    public TodoRepository(TodoContext db) => _db = db;
}

public class TodoContext : DbContext
{
    public DbSet<TodoEntity> Todos { get; set; }
    public TodoContext(DbContextOptions<TodoContext> opt) : base(opt) { }
}

public class TodoEntity
{
    public int Id { get; set; }
}
{
  "ConnectionStrings": {
    "DbName":  "test"
  }
}

起動すると...

こんなエラーが出ました。

f:id:piyo_esq:20200104225630p:plain
Service構築失敗のエラー

ログはこんな感じ

System.AggregateException
  HResult=0x80131500
  Message=Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity] Lifetime: Singleton ImplementationType: EFCore.Sample.Services.TodoRepository': Cannot consume scoped service 'EFCore.Sample.Contexts.TodoContext' from singleton 'EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity]'.) (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: EFCore.Sample.Worker': Cannot consume scoped service 'EFCore.Sample.Contexts.TodoContext' from singleton 'EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity]'.)
  Source=Microsoft.Extensions.DependencyInjection
  スタック トレース:
   ...

  この例外は、最初にこの呼び出し履歴でスローされました:
   ...

内部例外 1:
InvalidOperationException: Error while validating the service descriptor 'ServiceType: EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity] Lifetime: Singleton ImplementationType: EFCore.Sample.Services.TodoRepository': Cannot consume scoped service 'EFCore.Sample.Contexts.TodoContext' from singleton 'EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity]'.

内部例外 2:
InvalidOperationException: Cannot consume scoped service 'EFCore.Sample.Contexts.TodoContext' from singleton 'EFCore.Sample.Services.IRepository`1[EFCore.Sample.Entities.TodoEntity]'.

このエラーはなんぞ?

調べた結果、本来Singletonで登録しなければいけないサービスを、Singleton以外の生存期間でDI Containerに登録すると発生するらしい。

参考: Cannot Consume Scoped Service From Singleton – A Lesson In ASP.net Core DI Scopes

AddHostedService()がSingletonだからか…。

なおしかた

仕方ないのでservices.AddDbContextの第2引数にSingleton Lifetimeを指定する。
これでDbContextを継承したTodoContextがSingletonでDI Containerに登録される。

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                var con = hostContext.Configuration.GetConnectionString("DbName");
-               services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase(con));
+               services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase(con), ServiceLifetime.Singleton);
                services.AddSingleton<IRepository<TodoEntity>, TodoRepository>();
                services.AddHostedService<Worker>();
            });

参考1:Microsoft Docs API

DbContextをnewして使う場合はIServiceScopeFactoryCreateScopeでスコープを作ったりして回避するっぽい。

参考2:https://stackoverflow.com/questions/48368634/how-should-i-inject-a-dbcontext-instance-into-an-ihostedservice

おわり

コンパクトにまとまった良い本でした📘✨