【问题标题】:Cannot consume scoped service xxx from singleton yyy无法使用单例 yyy 中的作用域服务 xxx
【发布时间】:2019-03-24 16:14:30
【问题描述】:

我关注了一篇关于“使用 ASP.NET Core 和 Visual Studio Code 构建您的第一个 Web API”的博文。

http://www.codingflow.net/building-your-first-web-api-with-asp-net-core-and-visual-studio-code/

在这种情况下,数据不会保存在数据库中,而是保存在内存中,如下所示:

services.AddDbContext<TodoContext>(options => options.UseInMemoryDatabase());
services.AddSingleton<ITodoRepository, TodoRepository>();

你会注意到:

(1) UseInMemoryDatabaseDbContext

(2)AddSingletonTodoRepository

这很好用。现在我更新了代码以将数据保存在真实数据库中。所以主要变化是:

services.AddDbContext<TodoContext> (options => options.UseSqlite("Data Source=blogging.db"));            
services.AddSingleton<ITodoRepository, TodoRepository>();

我想通知我必须将 AspNetCore 从 1.0 迁移到 2.2。

现在在运行时,当定位控制器时,我收到错误:Cannot consume scoped service 'Models.TodoContext' from singleton 'Models.ITodoRepository'。

我明白在这种情况下:

  • 我的 TodoContext 是一个 Scoped 对象:在请求中相同,但在不同请求中不同。

  • 我的TodoRepository 是一个单例对象:每个对象和每个请求都相同。

所以我终于将AddSingleton 更改为AddScoped,效果很好:

services.AddDbContext<TodoContext> (options => options.UseSqlite("Data Source=blogging.db"));            
services.AddScoped<ITodoRepository, TodoRepository>();

我的问题是:知道这是否是一种可接受的方法?

PS:我知道关于这个问题还有其他关于 SO 的问题,但我没有阅读明确的回复。

【问题讨论】:

  • 不仅可以接受,而且非常可取。内存数据库是异常值。您希望存储库的范围仅限于请求,有时甚至更小。

标签: c# asp.net-core dependency-injection


【解决方案1】:

内置依赖注入容器的 ASP.NET 核心可保护您免受称为“强制依赖”的依赖注入反模式(您可以阅读有关它的更多信息以及一般的依赖注入 here)。

基本思想是具有一定生命周期的类只能依赖于生命周期等于或长于其自身生命周期的对象。这是因为当您进行依赖注入时,您提供了一个类及其所有依赖项(通常通过构造函数注入),并且该类保存对依赖项的引用,以便稍后在需要时可以使用它。

因为您正在设计的类保存了对注入对象的引用,所以只要您的类的实例还活着,注入的对象就会(至少)保持活动状态。也许一个例子可以帮助你理解。

public interface IFoo {}

public class Foo: IFoo {}

public class Bar 
{
  private readonly IFoo foo;

  public Bar(IFoo foo)
  {
    this.foo = foo ?? throw new ArgumentNullException(nameof(foo));
  }
}

var foo = new Foo();
var bar = new Bar(foo); // the object referenced by foo variable is alive as long as the Bar instance is alive, because a reference to it is saved inside the private field of Bar instance

如果 Foo 实例的预期生命周期短于 Bar 实例的预期生命周期,这种情况可能会给您带来麻烦。

例如,想象在 Web 应用程序的上下文中,并假设 Foo 类不是线程安全的,因此从不同线程同时访问它的实例可能会导致其私有状态的损坏。在这种情况下,您可以决定将 Foo 类注册为作用域依赖项,以便每次应用程序接收到 HTTP 请求时,都会创建一个新的 Foo 实例,并且该实例将在 HTTP 请求的整个生命周期内重复使用。这样做很好,即使处理 HTTP 请求意味着使用一些异步操作。假设您等待每个涉及的异步操作,最多只能有一个线程同时访问您的 Foo 实例,并且实例的内部状态被保留以防损坏。

在这种情况下,如果您将 Bar 类注册为单例,那么在应用程序的整个生命周期内可能只有一个 Bar 实例,因此不同的线程将同时访问 Bar 实例(请记住,Web 应用程序能够使用线程池同时服务多个请求)。您的 Bar 单例实例引用了 Foo 的实例,该实例可能会被多个线程同时使用,这将导致其内部状态的损坏和不可预测的结果。你有一个俘虏依赖,因为你有一个单例(Bar 类),它依赖于一个生命周期较短的类(作用域 Foo 类)。

回到您的问题,您的解决方案很好:您不能将您的存储库注册为单例,因为它必须使用范围依赖,因此在我看来,将其注册为范围服务是一种很好的方法。

【讨论】:

  • 非常感谢,很好的解释。很遗憾我不能多次投票。
【解决方案2】:

对于您的问题,是的,这是做事的正确方式。

我想以更好的方式总结答案,但后来我对该主题进行了一些研究,发现an article 可以处理您提出的问题。

我建议您看看这篇文章,因为它有关于如何解决某些问题的“最佳实践”部分。

【讨论】:

  • 谢谢你建议的文章对我有帮助。
猜你喜欢
  • 2022-09-29
  • 2019-03-08
  • 2019-06-14
  • 2013-01-23
  • 1970-01-01
  • 2023-02-12
  • 2019-04-27
  • 1970-01-01
  • 2011-08-09
相关资源
最近更新 更多