.net core 通过DI在IHostedService中利用用户上下文

.net core 通过DI在IHostedService中利用用户上下文,.net-core,asp.net-core-mvc,ihostedservice,.net Core,Asp.net Core Mvc,Ihostedservice,我有一系列的类库,它们用于asp.netcore中间件和IHostedService 要获取用户上下文,我可以插入IHttpContextAccessor来获取HttpContext用户: 公共类MyLibrary { 公共MyLibrary(IHttpContextAccessor访问器) { //设置访问器-没问题 } 公共异步任务DoWorkAsync(SomeObject负载) { //从访问器获取用户 //做些工作 } } 更抽象一点,我有一个带有HttpUserAccessor实

我有一系列的类库,它们用于
asp.netcore
中间件和
IHostedService

要获取用户上下文,我可以插入
IHttpContextAccessor
来获取
HttpContext
用户:

公共类MyLibrary
{
公共MyLibrary(IHttpContextAccessor访问器)
{
//设置访问器-没问题
}
公共异步任务DoWorkAsync(SomeObject负载)
{ 
//从访问器获取用户
//做些工作
}
}
更抽象一点,我有一个带有
HttpUserAccessor
实现的
IUserAccessor

公共类HttpUserAccessor:IUserAccessor
{
IHttpContextAccessor\uHttpAccessor;
公共HttpUserAccessor(IHttpContextAccessor访问器)
{
_httpaccessor=存取器;
}
公共字符串GetUser()
{
//从_httpaccessor返回用户
}
}
然后,
MyLibrary
不需要
IHttpContextAccessor
依赖项:

公共类MyLibrary
{
公共MyLibrary(IUserAccessor存取器)
{
//设置访问器-没问题
}
公共异步任务DoWorkAsync(SomeObject负载)
{ 
//从访问器获取用户
//做些工作
}
}
My
IHostedService
正在从队列中弹出消息,其中消息包括:

  • 用户上下文,以及
  • 要传递给MyLibrary.DoWorkAsync的序列化的
    SomeObject
比如说:

公共类MyHostedService:IHostedService
{
IServiceScopeProvider\u服务范围工厂;
公共MyHostedService(IServiceScopeFactory服务ScopeFactory)
{
_serviceScopeFactory=servicesScopeFactory;
}
公共任务StartSync(CancellationToken CancellationToken)
{ ... }
公共任务StopAsync(CancellationToken CancellationToken)
{ ... }
公共异步任务ExecuteAsync(CancellationToken stoppingToken)
{
foreach(队列中的var消息)
{
使用(var scope=\u serviceScopeFactory.CreateScope())
{
//todo:告诉IUserAccessor用户是什么消息!
var payload=//从队列消息创建一个SomeObject
var mylibrary=_services.GetRequiredService();
等待myLibrary.DoWorkAsync(有效负载);
}
}
}
}
因此,我的问题是,
MyHostedService
如何存储
消息。用户
以使自定义
IUserAccessor
可以通过DI以线程安全的方式访问它

MyHostedService如何存储message.User,使自定义IUserAccessor可以通过DI以线程安全的方式访问它

您要查找的是
asynchlocal
——它类似于线程局部变量,但作用域是(可能是异步的)代码块,而不是线程

我倾向于使用“提供者”+“存取者”配对:一种类型提供值,另一种类型读取值。这与JS世界中的React上下文在逻辑上是相同的,尽管实现方式有很大不同

AsyncLocal
的一个棘手问题是,您需要在任何更改中覆盖它的值。在这种情况下,这不是一个真正的问题(没有消息处理需要更新“用户”),但在一般情况下,记住这一点很重要。我更喜欢将不可变类型存储在
AsyncLocal
中,以确保它们不会直接变异,而不是覆盖值。在本例中,您的“用户”是一个
字符串
,它已经是不可变的,所以这是完美的

首先,您需要定义实际的
AsyncLocal
,以保存用户值并定义一些低级访问器。我强烈建议使用
IDisposable
,以确保
AsyncLocal
值在作用域结束时正确取消设置:

public static class AsyncLocalUser
{
  private static AsyncLocal<string> _local = new AsyncLocal<string>();
  private static IDisposable Set(string newValue)
  {
    var oldValue = _local.Value;
    _local.Value = newValue;

    // I use Nito.Disposables; feel free to replace with another IDisposable implementation.
    return Disposable.Create(() => _local.Value = oldValue);
  }
  private static string Get() => _local.Value;
}
访问器也同样简单:

public static class AsyncLocalUser
{
  ... // see above
  public sealed class Accessor : IUserAccessor
  {
    public string GetUser() => Get();
  }
}
您需要将DI设置为指向
IUserAccessor
AsyncLocalUser.Accessor
。您还可以选择向DI添加
AsyncLocalUser.Provider
,也可以直接创建它

用法如下所示:

foreach (var message in queue) 
{
  using (var scope = _serviceScopeFactory.CreateScope()) 
  {
    var userProvider = new AsyncLocalUser.Provider(); // (or get it from DI)
    using (userProvider.SetUser(message.User))
    {
      var payload = // create a SomeObject from the queue message
      var mylibrary = _services.GetRequiredService<MyLibrary>();
      await myLibrary.DoWorkAsync(payload);
    }
  }
}
foreach(队列中的var消息)
{
使用(var scope=\u serviceScopeFactory.CreateScope())
{
var userProvider=new asynchLocalUser.Provider();/(或从DI获取)
使用(userProvider.SetUser(message.User))
{
var payload=//从队列消息创建一个SomeObject
var mylibrary=_services.GetRequiredService();
等待myLibrary.DoWorkAsync(有效负载);
}
}
}

注入
IServiceProvider
是一种代码气味。您应该使用
IServiceScopeFactory
,但请记住,您所做的工作在请求管道之外。结果可能会有所不同,或者根本不起作用。您不应该在类似于Singleton的东西中依赖作用域服务,
IHostedService
就是这样。“还有一些书要读吗?”安迪,说得好。我已经更新了问题中的示例
IHostedService
。您关于在IHostedService中管理作用域服务的观点正是我的问题:什么是“正确”的方法?谢谢!我在
IHttpContextAccessor
中看到了
AsyncLocal
的使用,但是
Set
的用法我不清楚。这就澄清了这一点——我很快就会尝试一下!
foreach (var message in queue) 
{
  using (var scope = _serviceScopeFactory.CreateScope()) 
  {
    var userProvider = new AsyncLocalUser.Provider(); // (or get it from DI)
    using (userProvider.SetUser(message.User))
    {
      var payload = // create a SomeObject from the queue message
      var mylibrary = _services.GetRequiredService<MyLibrary>();
      await myLibrary.DoWorkAsync(payload);
    }
  }
}