【问题标题】:Create scope using IServiceProvider from singleton instance使用来自单例实例的 IServiceProvider 创建范围
【发布时间】:2020-10-12 14:27:31
【问题描述】:

假设我有自己的类QueueListener<TService, TPayload> 继承自BackgroundService。它打开 TCP 连接并监听传入的消息。在每条消息上,我想初始化 TService 类型的服务并将反序列化的 JSON 实例 TPayload 传递给它。 TService 将被注册为 Transient,因此这意味着作为有效负载的处理程序必须是轻量级和无状态的(在我当前的任务中)。为此,我将在我的QueueListener 的构造函数中注入IServiceProvider,并在它收到的每条消息上创建一个范围。这听起来像是一个计划还是我过度设计?我想避免TService 也是单例。

文档says:

从单例解析作用域服务是很危险的。可能导致服务在处理后续请求时状态不正确。

但我不完全确定这意味着什么。无法在BackgroundService 中注入作用域服务,因为它具有单例生命周期。他们是否警告我不要再像现在这样做事了?

UPD #1

我解释了为什么我要为每条消息创建范围。其背后的想法是防止侦听器被消息处理阻塞,并为其他开发人员提供创建自己的处理程序并对收到的消息做一些事情的可能性。例如,其他开发人员可以在处理时创建数据库连接,我希望在处理完成后关闭并释放它。

【问题讨论】:

  • 你没有说你为什么认为你需要一个范围?您的 TService 已注册为具有瞬态生命周期,这不需要范围。

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


【解决方案1】:

TService 注册为作用域并为每条消息创建一个新作用域。然后从创建的范围解析TService。请阅读Consuming a scoped service in a background task

你可以这样写:

services.AddHostedService<MyBackgroundService>();
services.AddScoped<IScopedServicePerMessage, ScopedServicePerMessage>();

...

public class MyBackgroundService : BackgroundService
{
    private readonly IServiceProvider _sp;
    public MyBackgroundService(IServiceProvider sp)
    {
        _sp = sp;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        DoWork(stoppingToken);
        return Task.CompletedTask;
    }

    private void DoWork(CancellationToken stoppingToken)
    {
        while(true)
        {
            var msg = GetNextMessage();
            using (var scope = _sp.CreateScope())
            {
                var servicePerMessage = scope.ServiceProvider.GetRequiredService<IScopedServicePerMessage>();
                servicePerMessage.Handle(msg);
            }
        }
    }
    ...
}

关于这个:

从单例解析作用域服务是很危险的。它可能 导致服务在处理后续请求时状态不正确。

这是关于将作用域服务(例如,ef core dbcontext)直接注入单例的情况。这不是你的情况。

【讨论】:

  • 您好,谢谢您的留言。但我无法联系到您提供的参考资料。
  • 嗬嗬!正是这种情况,同时也是我问题的答案!太感谢了。 weichchpoke 的任何方式都帮助我完整地提出了我的问题。谢谢大家!
  • 老实说,我不明白这如何为现有答案添加任何内容。重要信息,即如何创建范围并从中解析服务,隐藏在通常不鼓励使用的链接后面。
  • @poke 这个链接包含关于这种情况的明确信息。是不是复制到这里比较好?
【解决方案2】:

文档是指将范围内的服务注入到单例服务中。由于注入发生在单例对象的构造中,因此将在那时提供作用域服务。这将有效地将作用域服务的生命周期延长到单例服务的生命周期。这是很危险的,因为通常会明确选择范围服务生命周期以确保对象再次被快速释放。

最常见的例子是拥有数据库连接的数据库上下文;您要确保尽快释放此数据库连接以释放资源。但是如果你将上下文注入到单例服务中,它永远不会被释放。

但这并不意味着无法在单例服务中使用作用域服务。这是通过让单例服务创建一个服务范围来完成的,然后它可以从中检索单例服务。重要的是,这个服务范围应该是短暂的。因此,以 ASP.NET Core 本身为例,其中为每个请求创建一个服务范围,并执行类似的操作。例如,在您的情况下,如果这对您的应用程序有意义,您可以为每条传入消息执行此操作。

要创建一个服务范围,你应该注入一个IServiceScopeFactory;然后你可以像这样用它创建一个范围:

public async Task Process(TPayload payload)
{
    using (var scope = _serviceScopeFactory.CreateScope())
    {
        var service = scope.GetService<TService>();

        await service.Process(payload);
    }
}

仅当您需要使用作用域服务时,才严格需要此模式。您可以直接解析所有其他服务,而无需创建范围。如果您可以重用相同的服务实例来处理所有有效负载,您还可以将服务作为单例注入(与将其注册为瞬态但仅解析一次相同)。如果您需要为每个有效负载创建一个新实例,那么请考虑创建一个范围,即使它不是绝对必要的。

【讨论】:

  • 我不认为第一句中的 injecting 是好的词选择,因为它可能指构造函数注入、属性注入等。我认为 resolve 会是更好的选择。作用域服务被缓存在它被解析的生命周期范围内。如果作用域服务是从单例服务中解析出来的,那么很可能用于解析服务的生命周期是根作用域(容器本身),因此服务被缓存在容器中,从而延长了作用域服务的生命周期。跨度>
  • 感谢您的回复!如果您尝试从单例构造函数解析作用域服务,Dotnet 会抛出 InvalidOperationException。所以我认为不可能使用范围服务,除非您像描述的那样自己创建范围。我想在每条消息上创建一个新范围的原因是为其他开发人员编写自己的消息处理程序提供了可能性。我想这让他们可以轻松而清晰地编写自己的TService 并异步调用方法。这样他们就不会阻塞他们的监听器,而是运行多个消息处理。
  • @weichch 我故意选择 injecting 是因为当您通过服务定位器解析服务时,解析的服务不会自动绑定到调用者的生命周期。我的示例在这里展示了如何在单例服务中解析一个作用域服务,而不将生命周期绑定到父组件;通过不同的(范围)服务提供商。
  • @matterai 是的,例外是一种验证机制,默认情况下启用,它将检测对单例的作用域依赖。如果您禁用该验证,那么它可以注入作用域服务,但这不会改变已解决的服务不再具有作用域生命周期。所以是的,在这里创建一个显式范围是正确的。
【解决方案3】:

首先,transient services are not scoped services。瞬态服务通常由您的代码在外部拥有,并在每次从容器中解析时创建。容器不缓存瞬态服务。

TService 将被注册为 Transient ... 为此,我将在我的 QueueListener 的构造函数中注入 IServiceProvider 并在它收到的每条消息上创建一个范围。

您不需要解析瞬态服务的范围。即使您创建了一个范围,该范围仍然不管理/拥有瞬态服务。例如,结束作用域的生命周期并不会结束瞬态服务的生命周期。

您可以简单地使用QueueListener 中注入的IServiceProvider 来解析TService。并且每个TService 解决的都应该已经是你想要的了

作为有效载荷处理程序的轻量级和无状态

关于

文档说:

由于您没有使用范围服务,因此文档中的内容现在可能不相关。但如果你想知道原因:

从单例解析作用域服务很危险。

单例是一种特殊的作用域。单例服务在容器的“根”范围内创建和缓存,本质上是容器本身。

如果您从单例解析范围服务,则解析和缓存服务实例的生命周期/范围很可能是“根”范围。这导致了一个问题,即作用域服务实例被缓存在容器内,并在多个客户端请求之间共享。

这是危险的,因为范围服务应该是

每个客户端请求(连接)创建一次范围生命周期服务 (AddScoped)。

【讨论】:

  • 感谢您的回复。首先,我在帖子中附加了一些关于我为什么需要范围的建议的新信息。您写道,我可以使用IServiceProviderTService 解析为任何我想要的。我认为这并不完全正确,因为如果TService 注册为范围服务,dotnet 会抛出InvalidOperationException。它应该注册为单例,作为它运行的QueueListener
  • 我的回答是基于你原来的问题,你说 TService 将被注册为 Transient。要仅解析瞬态服务,您不需要范围。但是您的更新现在更有意义,好像 TService 具有依赖范围的服务,那么您需要一个范围。
  • 在这种情况下,@poke 的答案就是您要找的。​​span>
【解决方案4】:

我不希望我的单例类直接依赖于 IServiceProvider。 所以我使用了一个定制工厂来实现这个目标。 愿此代码示例对其他人有所帮助:

public class Startup
{
    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IScopedBar, ScopedBar>();

        services.AddSingleton<IScopedServiceFactory<IScopedBar>, ScopedServiceFactory<IScopedBar>>(
            (provider) => {
                var scope = provider.CreateScope();
                var service = scope.ServiceProvider.GetRequiredService<IScopedBar>();
                
                return new ScopedServiceFactory<IScopedBar>(() => new ScopedService<IScopedBar>(scope, service));
            });

        services.AddSingleton<ISingletonFoo, SingletonFoo>();
    }

    // ...
}

public interface ISingletonFoo
{
    void DoSomethingUsingScopedServices();
}

public class SingletonFoo : ISingletonFoo
{
    private readonly IScopedServiceFactory<IScopedBar> _barFactory;

    public SingletonFoo(IScopedServiceFactory<IScopedBar> barFactory)
    {
        _barFactory = barFactory;
    }

    public void DoSomethingUsingScopedServices()
    {
        using var scopedService = _barFactory.CreateService();
        scopedService.Service.DoSomething();
    }
}

public interface IScopedBar
{
    void DoSomething();
}

public class ScopedBar : IScopedBar
{
    public void DoSomething()
    {
        // Do something
    }
}

public interface IScopedService<T> : IDisposable
{
    T Service { get; }
}

public interface IScopedServiceFactory<T>
{
    IScopedService<T> CreateService();
}

public class ScopedService<T> : IScopedService<T>
{
    private readonly IDisposable _scope;
    
    public ScopedService(IDisposable scope, T service)
    {
        _scope = scope;
        Service = service;
    }

    public T Service { get; }

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

public class ScopedServiceFactory<T> : IScopedServiceFactory<T>
{
    private readonly Func<IScopedService<T>> _serviceFactory;

    public ScopedServiceFactory(Func<IScopedService<T>> serviceFactory)
    {
        _serviceFactory = serviceFactory;
    }

    public IScopedService<T> CreateService()
    {
        return _serviceFactory();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多