Home > Mobile >  .Net 5 Console Background service exception with DI of Entity Framework Core
.Net 5 Console Background service exception with DI of Entity Framework Core

Time:11-21

I have a .NET 5 console application that I am trying to use as a Windows Service which is in the form of a background service now. I have all of the dependency injection setup and working for appsettings and logging just fine. Today, I wanted to add my data into the mix. Here is the structure of my application:

  1. I have the Entity Framework Core DataContext class all complete.

  2. I then created a wrapper, which I will use to create all of my CRUD methods and use within my app. This class is named DataAccess.

  3. Then, I added the following to my Program.cs, Main method:

    services.AddSingleton<DataAccess>();
    services.AddDbContext<MyDataContext>(options => options.UseSqlServer(ConnnectionString));
    
  4. Finally, in my DataAccess wrapper class, I inject the DataContext so I can do my CRUD operations here. Like this:

     public DataAccess(MyDataContext dataAccess)
     {
         this.dataAccess = dataAccess;
     }
    

When I do this and debug, I get the following exception when my app starts:

Cannot consume scoped service 'ENS.Data.MyDataContext' from singleton 'ENS.Data.DataAccess'.

I know my DataAccess wrapper is a singleton and I assume that when you do services.AddDbContext that is scoped. So, I sort of understand the error. What I don't understand is I thought you could consume a scoped from a singleton?

What am I doing wrong here or better yet, what is the best way to setup and inject Entity Framework Core DataContext into my application for use by my DataAccess wrapper and then my DataAccess wrapper for use within my whole app?

Thanks!

CodePudding user response:

When you inject a service inside of another service you need to carefully think of the lifetime of the two services. If you inject service A into service B, then the lifetime of service A must be greater than or equal to the lifetime of service B (because service B will hold a reference on service A, preventing the garbage collector to reclaim the memory allocated for service A).

Based on this rule, you can safely inject a singleton inside of a scoped service, but the opposite is not allowed (because the lifetime of a scoped service is shorter than the lifetime of a singleton service). The violation of this rule leads to an anti pattern known as captive dependency. If injecting a scoped service into a singleton service were allowed, then the scoped service would become a captive dependency and its lifetime would coincide with the one of the singleton service (put in other words, the injected scoped service would actually become a singleton).

The Microsoft DI container explicitly checks for this anti pattern, this is why you are getting an error when trying to inject a scoped service into a singleton service.

In your case you have two choices:

  1. avoid the data access wrapper class and directly inject the entity framework context where you need it
  2. maintain the data access wrapper class and give it a scoped or transient lifetime, so that you can inject the scoped entity framework context into it

As a sidenote, if you need to consume a scoped service from an hosted service class, you can inject the IServiceScopeFactory service in the hosted service and use it to create a scope and resolve the scoped service.

Here is an example:

public sealed class MyHostedService: BackgroundService 
{
  private readonly IServiceScopeFactory _scopeFactory;
  
  public MyHostedService(IServiceScopeFactory scopeFactory)
  {
    _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
  }
  
  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    // ...
  
    using (var scope = _scopeFactory.CreateScope())
    {
      var myScopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
      myScopedService.DoWork();
    }
    
    // ....
  }
}

You can find more details here

CodePudding user response:

What I don't understand is I thought you could consume a scoped from a singleton?

Then here is the explanation: You thought wrong. Your thought actually makes no sense.

  • DataAccess is a singleton. There is EVER ONLY ONE.
  • The context is inserted into the constructor.

As the context can have multiple - that would mean that the first would be inserted into the data access. YOu would never be able to use another one.

Generally - besides DataAccess as repository being an epic antipattern - there is no sense in injecting the db context UNLESS YOU HAVE ONLY ONE DataAcess instance anyway, and then you could or should have a singleton context too.

The thought makes literally no sense. Now, I would get rid of the DataAccess (or fire you), but that aside, you have 2 ways to handle this:

  • Accept you only ever have one data access, inject a singleton dbcontext into it.
  • Make both scoped if you need scope.
  • Related