C# 使用IServiceProvider从单例实例创建作用域

C# 使用IServiceProvider从单例实例创建作用域,c#,asp.net-core,.net-core,dependency-injection,C#,Asp.net Core,.net Core,Dependency Injection,假设我从BackgroundService继承了自己的类QueueListener。它打开TCP连接并侦听传入消息。在每条消息上,我想初始化TService类型的服务,并将TPayload的JSON实例的反序列化传递给它。TService将被注册为Transient,因此它意味着轻量级和无状态,因为有效负载的处理程序必须在我当前的任务中。为此,我将在QueueListener的构造函数中注入IServiceProvider,并在它接收的每个消息上创建一个作用域。这听起来像是一个计划还是我设计过度

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

文件:

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

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

UPD 1


我解释了为什么我要在每条消息上创建作用域。其背后的想法是防止侦听器被消息处理阻塞,并为其他开发人员提供创建自己的处理程序和对接收到的消息执行某些操作的可能性。其他开发人员可以在处理过程中创建数据库连接,我希望在处理完成时关闭并释放数据库连接。

该文档指的是将作用域服务注入到单例服务中。因为注入发生在singleton对象的构造中,所以此时将提供作用域服务。这将有效地将作用域服务的生存期增加到单例服务的生存期。这是危险的,因为通常会显式选择作用域服务生存期,以确保再次快速释放对象

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

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

要创建服务范围,您应该注入IServiceScopeFactory;然后,您可以使用它创建一个作用域,如下所示:

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

        await service.Process(payload);
    }
}
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);
            }
        }
    }
    ...
}

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

< P>文档是指将范围化的服务注入到单服务中。因为注入发生在singleton对象的构造中,所以此时将提供作用域服务。这将有效地将作用域服务的生存期增加到单例服务的生存期。这是危险的,因为通常会显式选择作用域服务生存期,以确保再次快速释放对象

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

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

要创建服务范围,您应该注入IServiceScopeFactory;然后,您可以使用它创建一个作用域,如下所示:

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

        await service.Process(payload);
    }
}
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);
            }
        }
    }
    ...
}
仅当您需要使用作用域服务时,才需要此模式。您可以直接解析所有其他服务,而无需创建作用域。如果可以重用同一个服务实例来处理所有有效负载, 您还可以将服务作为单例注入,就像将其注册为瞬态一样,但只解析一次。如果每一个有效载荷需要一个新的实例,那么就要考虑创建一个范围,即使它不是严格必要的。临时服务通常由您的代码在外部拥有,并且每次从容器解析时都会创建临时服务。容器不缓存临时服务

TService将被注册为Transient。。。为此,我将在QueueListener的构造函数中注入IServiceProvider,并在它接收的每个消息上创建一个作用域

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

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

作为负载处理程序的轻量级和无状态

关于

文件说:

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

从单例解析作用域服务是危险的

Singleton是一种特殊的范围。单例服务是在容器的根范围内创建和缓存的,它本质上就是容器本身

如果从singleton解析作用域服务,则解析和缓存服务实例的生存期/作用域可能是根作用域。这会导致一个问题,即作用域服务实例被缓存在容器中,并在多个客户端请求之间共享

这是危险的,因为作用域服务应该是

作用域生存期服务AddScoped每个客户端请求连接创建一次

首先。临时服务通常由您的代码在外部拥有,并且每次从容器解析时都会创建临时服务。容器不缓存临时服务

TService将被注册为Transient。。。为此,我将在QueueListener的构造函数中注入IServiceProvider,并在它接收的每个消息上创建一个作用域

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

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

作为负载处理程序的轻量级和无状态

关于

文件说:

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

从单例解析作用域服务是危险的

Singleton是一种特殊的范围。单例服务是在容器的根范围内创建和缓存的,它本质上就是容器本身

如果从singleton解析作用域服务,则解析和缓存服务实例的生存期/作用域可能是根作用域。这会导致一个问题,即作用域服务实例被缓存在容器中,并在多个客户端请求之间共享

这是危险的,因为作用域服务应该是

作用域生存期服务AddScoped每个客户端请求连接创建一次

将TService注册为作用域,并为每条消息创建一个新的作用域。然后从创建的范围解析TService。读一下

你可以这样写:

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

        await service.Process(payload);
    }
}
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直接注入singleton的情况。这不是您的情况。

将TService注册为作用域,并为每条消息创建一个新的作用域。然后从创建的范围解析TService。读一下

你可以这样写:

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

        await service.Process(payload);
    }
}
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直接注入singleton的情况。这不是你的情况。

我不希望我的单例类直接依赖于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();
    }
}

我不希望我的单例类直接依赖IServiceProvider。 所以我使用了一个定制工厂来实现这个目标。 此代码示例是否可以帮助其他人:< /p>
你没有说为什么你认为你需要一个范围?您的TService已注册为具有瞬态生存期,这不需要作用域。您没有说您为什么认为需要作用域?您的TService已注册为具有瞬态生存期,这不需要作用域。我不认为第一句中的注入是好的词汇选择,因为它可能指构造函数注入、属性注入等。我认为解析将是更好的选择。作用域服务缓存在其解析的生存期作用域内。如果作用域服务是从单例服务解析的,则用于解析该服务的生存期很可能是容器本身的根作用域,因此该服务被缓存在容器中,从而扩展了作用域服务的生存期。感谢您的回复!如果您试图从singleton构造函数解析作用域服务,Dotnet将引发InvalidOperationException。因此,我假设不可能使用作用域服务,除非您像前面描述的那样自己创建作用域。我之所以想在每条消息上创建一个新的作用域,是为了让其他开发人员能够编写自己的消息处理程序。我想这给了他们编写自己的TService和异步调用方法的简单而清晰的方法。这样他们就不会阻止侦听器,而是运行多个消息处理。@weichch我故意选择注入,因为当您通过服务定位器解析服务时,解析的服务不会自动绑定到调用方的生存期。我这里的示例展示了如何在单例服务中解析作用域服务,而无需将生存期绑定到父组件;通过使用不同的作用域服务提供程序。@matterai是的,异常是一种验证机制,默认情况下是启用的,它将检测单例上的作用域依赖项。如果禁用该验证,则可以注入作用域服务,但这不会改变解析的服务不再具有作用域生存期。是的,在这里创建一个显式范围是正确的。我不认为第一句中的注入是一个好的词汇选择,因为它可能指构造函数注入、属性注入等。我认为解析是更好的选择。作用域服务缓存在其解析的生存期作用域内。如果作用域服务是从单例服务解析的,则用于解析该服务的生存期很可能是容器本身的根作用域,因此该服务被缓存在容器中,从而扩展了作用域服务的生存期。感谢您的回复!如果您试图从singleton构造函数解析作用域服务,Dotnet将引发InvalidOperationException。因此,我假设不可能使用作用域服务,除非您像前面描述的那样自己创建作用域。我之所以想在每条消息上创建一个新的作用域,是为了让其他开发人员能够编写自己的消息处理程序。我想这给了他们编写自己的TService和异步调用方法的简单而清晰的方法。这样他们就不会阻止侦听器,而是运行多个消息处理。@weichch我故意选择注入,因为当您通过服务定位器解析服务时,解析的服务不会自动绑定到调用方的生存期。我这里的示例展示了如何在单例服务中解析作用域服务,而无需将生存期绑定到父组件;通过使用不同的作用域服务提供程序。@matterai是的,异常是一种验证机制,默认情况下是启用的,它将检测单例上的作用域依赖项。如果禁用该验证,则可以注入作用域服务,但这不会改变解析的服务不再具有作用域生存期。是的,在这里创建显式作用域是正确的做法。谢谢您的回复。首先,我在我的帖子后面添加了一些关于我的建议的新信息,为什么我需要一个范围。您曾写道,我可以使用IServiceProvider根据需要解析TService。这并不像我假设的那样完全正确,因为如果TService注册为作用域服务,dotnet会抛出InvalidOperationException。它应该注册为它运行的QueueListener中的singleton。我的答案是基于您最初的问题,您说TService将注册为Transient。要仅解析临时服务,不需要作用域。但您的更新现在更有意义,因为如果TService有依赖作用域的服务,那么您需要一个作用域。在这种情况下,@poke的答案就是您要寻找的。谢谢您的回复。首先,我在我的帖子后面添加了一些关于我的建议的新信息,为什么我需要一个范围。您曾写道,我可以使用IServiceProvider根据需要解析TService。这并不像我想象的那样完全正确,因为dotnet抛出了InvalidOperati
如果TService注册为作用域服务,则为OneException。它应该注册为它运行的QueueListener中的singleton。我的答案是基于您最初的问题,您说TService将注册为Transient。要仅解析临时服务,不需要作用域。但是你的更新现在更有意义了,因为如果TService有依赖的作用域服务,那么你需要一个作用域。在这种情况下,@poke的答案就是你想要的。嘿,谢谢你的消息。但是我找不到你提供的参考资料。呵呵!这就是事实,同时也是对我问题的回答!非常感谢你。不管怎样,魏奇和普克都帮助我完整地阐述了我的问题。谢谢大家!老实说,我不认为这会给现有的答案增加什么。重要信息,即如何创建作用域并从中解析服务,隐藏在一个通常不鼓励使用的链接后面。@poke此链接包含关于这种情况的明确信息。把它复制到这里好吗?嘿,谢谢你的留言。但是我找不到你提供的参考资料。呵呵!这就是事实,同时也是对我问题的回答!非常感谢你。不管怎样,魏奇和普克都帮助我完整地阐述了我的问题。谢谢大家!老实说,我不认为这会给现有的答案增加什么。重要信息,即如何创建作用域并从中解析服务,隐藏在一个通常不鼓励使用的链接后面。@poke此链接包含关于这种情况的明确信息。把它抄在这里好吗?