0

我有一系列用于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
  }
}

为了更抽象一点,我有IUserAccessor一个HttpUserAccessor实现:

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正在从队列中弹出消息,其中消息包括:

  • 用户上下文,以及
  • 一个序列化SomeObject的传递给MyLibrary.DoWorkAsync

所以,像:

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存储,以便自定义可以通过 DI 以线程安全的方式访问它?message.User IUserAccessor

4

1 回答 1

1

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

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

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

一件棘手的事情AsyncLocal<T>是,您需要在任何更改时覆盖其值。在这种情况下,这并不是真正的问题(没有消息处理想要更新“用户”),但在一般情况下,记住这一点很重要。我更喜欢将不可变类型存储在 中,AsyncLocal<T>以确保它们不会直接变异,而不是覆盖值。在这种情况下,您的“用户”是 a 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);
    }
  }
}
于 2021-05-05T12:43:18.773 回答