93

I updated a project to ASP.NET Core 2 today and I get the following error:

Cannot consume scoped service IMongoDbContext from singleton IActiveUsersService

I have the following registration:

services.AddSingleton<IActiveUsersService, ActiveUsersService>();
services.AddScoped<IMongoDbContext, MongoDbContext>();
services.AddSingleton(option =>
{
   var client = new MongoClient(MongoConnectionString.Settings);
   return client.GetDatabase(MongoConnectionString.Database);
})



public class MongoDbContext : IMongoDbContext
{
   private readonly IMongoDatabase _database;

   public MongoDbContext(IMongoDatabase database)
   {
      _database = database;
   }

   public IMongoCollection<T> GetCollection<T>() where T : Entity, new()
   {
      return _database.GetCollection<T>(new T().CollectionName);
   }
}

public class IActiveUsersService: ActiveUsersService
{

   public IActiveUsersService(IMongoDbContext mongoDbContext)
   {
      ...
   }
}

Why DI can't consume the service? All works fine for ASP.NET Core 1.1.

Tseng
  • 61,549
  • 15
  • 193
  • 205
user348173
  • 8,818
  • 18
  • 66
  • 102
  • Why do you even want to have the Mongo context be scoped in the first place? Do you have some kind of tenant functionality, where you know the tenant id only at runtime? [MongoDatabase](https://mongodb.github.io/mongo-csharp-driver/2.4/apidocs/html/T_MongoDB_Driver_MongoDatabase.htm) is thread safe, so you can also keep it as singleton (assuming you don't run 100s of it which keep to many connections open). EF Core on the other side is not thread safe and has a caching mechanism on by default (EF Core tracking also acts as caching), so it's natural to have it scoped) – Tseng Aug 22 '17 at 17:32
  • Possible duplicate of [Use DbContext in ASP .Net Singleton Injected Class](https://stackoverflow.com/questions/36332239/use-dbcontext-in-asp-net-singleton-injected-class) – Michael Freidgeim Feb 19 '18 at 02:30

6 Answers6

135

You can't use a service with a smaller lifetime. Scoped services only exist per-request, while singleton services are created once and the instance is shared.

Now only one instance of IActiveUsersService exists in the app. But it wants to depend on MongoDbContext, which is Scoped, and is created per-request.

You will have to either:

  1. Make MongoDbContext a Singleton, or
  2. Make IActiveUsersService Scoped, or
  3. Pass MongoDbContext into the user service as a function argument
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
juunas
  • 54,244
  • 13
  • 113
  • 149
  • 2
    Make sense, but, what if I have another class, `MyService` for example, it's scoped and I want to have scoped `MongoDbContext` for this service. – user348173 Aug 22 '17 at 07:25
  • 1
    You can't have different lifetimes (without different interfaces for example). Why do you want to have a scoped version for that one? If a service can be singleton, it should be singleton. If it can't, but it can be scoped, it should be scoped. If it can't be scoped, it should be transient. – juunas Aug 22 '17 at 07:27
  • Thanks, makes totally sense - but I wonder why it was possible in ASP.NET Core 1.0? Was a bit confusing. – Matt Dec 11 '17 at 17:15
  • I think the error was only added in 2.0. In certain situations it may have worked, but basically you should not do it. – juunas Dec 11 '17 at 18:21
45

There are important differences between Scoped and Singleton services. The warning is there to bring this to light, and turning it off or switching around lifetimes indiscriminately to make it go away won't solve the problem.

Scoped services are created from an IServiceScope. One of its most important purposes is to ensure that any IDisposable services which are created in that scope are properly disposed when the scope itself is.

In ASP.NET Core, a service scope is automatically created for you on each incoming request, so you ordinarily don't need to worry about this. However, you can also create your own service scope; you just need to dispose of it yourself.

One way to do this is to:

  • make your singleton service IDisposable,
  • inject IServiceProvider,
  • create and store an IServiceScope scope using the IServiceProvider.CreateScope() extension method,
  • use that scope to create the the scoped service you need,
  • dispose the service scope in the Dispose method.
services.AddSingleton<IActiveUsersService, ActiveUsersService>();
services.AddScoped<IMongoDbContext, MongoDbContext>();
services.AddSingleton(option =>
{
   var client = new MongoClient(MongoConnectionString.Settings);
   return client.GetDatabase(MongoConnectionString.Database);
})

public class MongoDbContext : IMongoDbContext
{
   private readonly IMongoDatabase _database;

   public MongoDbContext(IMongoDatabase database)
   {
      _database = database;
   }

   public IMongoCollection<T> GetCollection<T>() where T : Entity, new()
   {
      return _database.GetCollection<T>(new T().CollectionName);
   }
}

public class ActiveUsersService: IActiveUsersService, IDisposable
{
   private readonly IServiceScope _scope;

   public ActiveUsersService(IServiceProvider services)
   {
      _scope = services.CreateScope(); // CreateScope is in Microsoft.Extensions.DependencyInjection
   }

   public IEnumerable<Foo> GetFooData()
   {
       using (var context = _scope.ServiceProvider.GetRequiredService<IMongoDbContext>())
       {
           return context.GetCollection<Foo>();
       }
   }

   public void Dispose()
   {
       _scope?.Dispose();
   }
}

Depending on how you're using these and the scoped services you're consuming, you could instead do one of the following:

  • create a single instance of the scoped service and use it for the life of the singleton; or
  • store a reference to the (injected) root IServiceProvider, use it to create a new IServiceScope inside a using block every time you need a scoped service, and let the scope get disposed when the block exits.

Just keep in mind that any IDisposable services created from an IServiceScope will get automatically disposed when the scope itself does.

In short, don't just change around the lifetimes of your services to "make it work"; you still need to think about those and be sure they get disposed properly. ASP.NET Core handles the most common cases automatically; for others, you just need to do a bit more work.

Ever since C# 1.0 we have had using() blocks to ensure resources are disposed correctly. But using() blocks don't work when something else (the DI service) is creating those resources for you. That's where Scoped services come in, and using them incorrectly will lead to resource leaks in your program.

Tobias J
  • 19,813
  • 8
  • 81
  • 66
  • 1
    Is `BeginScope()` equivalent to `ServiceProviderServiceExtensions.CreateScope()`? Perhaps it has been renamed? – g t May 14 '18 at 11:17
  • 1
    @gt yes it is, not sure if it was renamed or I just mis-typed it but I fixed the example & added link, thanks! – Tobias J May 14 '18 at 13:57
10

You can also add

.UseDefaultServiceProvider(options =>
                    options.ValidateScopes = false)

before .Build() in Program.cs file to disable the validation.

Try this only for development testing, ActiveUsersService is singleton and has a larger lifetime than MongoDbContext which is scoped and will not get disposed.

Robin Thomas
  • 535
  • 5
  • 6
6

There is another way to approach this issue, and it is by adding the MongoDbContext to the DI as AddTransient like this:

services.AddSingleton<IActiveUsersService, ActiveUsersService>();
services.AddTransient<IMongoDbContext, MongoDbContext>();

The meaning of using this approach is that you'll end up with an instance of MongoDbContext for each Singleton class you have using it. For example, if you have 10 Singleton classes using MongoDbContext you'll have 10 instances of it, but it's instead of creating an instance for every request.

See this for reference: Cannot Consume Scoped Service From Singleton – A Lesson In ASP.net Core DI Scopes

Liran Friedman
  • 4,027
  • 13
  • 53
  • 96
3

Since some input parameters of the constructor are not interface, engine can not inject an object into the class.

use AddScoped rather than AddSingleton, it maybe resolves the issue.

Hamid Jolany
  • 800
  • 7
  • 11
2

From Constructor Injection, a long Lifetime service can't request a shorter Lifetime service..

The reason is that the longer lifetime service will use the shorter lifetime service for longer time than it was specified. The instance of shorter service can't be updated as often as needed.

The following options are allowed:

          | Transient | Scoped | Singleton
-------------------------------------------
Transient |     +     |    +   |     +
-------------------------------------------
Scoped    |     -     |    +   |     +
-------------------------------------------
Singleton |     -     |    -   |     +

This exception is only shown for Development environment and not Production

giorgi02
  • 683
  • 6
  • 13