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

Leveraging user context in an IHostedService via DI

我有一系列 class 库用于 asp.net-core 中间件和 IHostedService.

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

public class MyLibrary 
{
  public MyLibrary(IHttpContextAccessor accessor)
  {
    // set the accessor - no problem
  }
  
  public async Task DoWorkAsync(SomeObject payload) 
  { 
    // get the user from the accessor
    // do some work
  }
}

更抽象一点,我有一个 IUserAccessorHttpUserAccessor 实现:

public class HttpUserAccessor: IUserAccessor
{
  IHttpContextAccessor _httpaccessor;
  public HttpUserAccessor(IHttpContextAccessor accessor) 
  {
    _httpaccessor = accessor;
  }

  public string GetUser()
  {
    // return user from _httpaccessor
  }
}

然后 MyLibrary 不需要 IHttpContextAccessor 依赖项:

public class MyLibrary 
{
  public MyLibrary(IUserAccessor accessor)
  {
    // set the accessor - no problem
  }
  
  public async Task DoWorkAsync(SomeObject payload) 
  { 
    // get the user from the accessor
    // do some work
  }
}

我的 IHostedService 正在从队列中弹出消息,消息包括:

所以,类似于:

public class MyHostedService : IHostedService
{
  IServiceScopeProvider _serviceScopeFactory;

  public MyHostedService(IServiceScopeFactory serviceScopeFactory) 
  {
    _serviceScopeFactory = servicesScopeFactory;
  }

  public Task StartAsync(CancellationToken cancellationToken)
  { ... }

  public Task StopAsync(CancellationToken cancellationToken)
  { ... }

  public async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    foreach (var message in queue) 
    {
      using (var scope = _serviceScopeFactory.CreateScope()) 
      {
        // todo: tell IUserAccessor what message.User is!
        var payload = // create a SomeObject from the queue message
        var mylibrary = _services.GetRequiredService<MyLibrary>();
        await myLibrary.DoWorkAsync(payload);
      }
    }
  }
}

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

how does MyHostedService store message.User in such a way that a custom IUserAccessor can access it in a thread-safe manner via DI?

您正在寻找的东西是 AsyncLocal<T> - 它类似于线程局部变量,但作用域为(可能是异步的)代码块而不是线程。

为此,我倾向于使用“提供者”+“访问者”配对:一种提供值的类型,另一种读取值的单独类型。这在逻辑上与 JS 世界中的 React 上下文相同,尽管实现方式完全不同。

关于 AsyncLocal<T> 的一件棘手事情是您需要 覆盖 它的任何更改值。在这种情况下,这并不是真正的问题(没有消息处理会想要更新“用户”),但在一般情况下,记住这一点很重要。我更喜欢在 AsyncLocal<T> 中存储不可变类型,以确保它们不会直接发生突变,而不是覆盖值。在这种情况下,您的“用户”是一个 string,它已经是不可变的,所以这是完美的。

首先,您需要定义实际的 AsyncLocal<T> 来保存用户值并定义一些低级访问器。我强烈建议使用 IDisposable 以确保 AsyncLocal<T> 值在范围末尾正确取消设置:

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 Provider
  {
    public IDisposable SetUser(string value) => Set(value);
  }
}

访问器同样简单:

public static class AsyncLocalUser
{
  ... // see above
  public sealed class Accessor : IUserAccessor
  {
    public string GetUser() => Get();
  }
}

您需要将 DI 设置为将 IUserAccessor 指向 AsyncLocalUser.Accessor。您还可以选择将 AsyncLocalUser.Provider 添加到您的 DI,或者您可以直接创建它。

用法是这样的:

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);
    }
  }
}