通过 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
}
}
更抽象一点,我有一个 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
如何以自定义 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);
}
}
}
我有一系列 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
}
}
更抽象一点,我有一个 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
如何以自定义 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);
}
}
}