如何在后台任务中获取IHttpContextAccessor实例(或等效实例)?

4
在我的ASP.Net Core 3.1 Webapi中,我将 IHttpContextAccessor 注册为单例并注入到所有控制器中。我有一个接口也被注入到所有的控制器和服务中(这些服务连接到数据库)。该接口的实现如下:
public class PrincipalProvider : IPrincipalProvider
{
    private readonly UserPrincipal principal;

    public PrincipalProvider(IHttpContextAccessor accessor)
    {
        accessor.HttpContext.Items.TryGetValue("principal", out object principal);
        this.principal = principal as UserPrincipal;
    }

    public UserPrincipal GetPrincipal()
    {
        return principal;
    }
}

一个服务的构造函数如下:

    public MyService(
        IPrincipalProvider provider,
        ILogger<MyService> logger, 
        IUnitOfWork unitOfWork) : base(provider, logger, unitOfWork) 
    { }

只要我在请求上下文中,以上所有内容都能正常工作。

我有一个控制器操作,使用具有后台队列的新IHostedService实现启动后台任务,并且它是这样启动的:

backgroundQueue.QueueBackgroundWorkItem(async (scope, hubContext, ct) =>
{
    await hubContext.Clients.Client(provider.GetPrincipal().ConnectionId).Notify();
    var myService = scope.Resolve<IMyService>();
}

scopeILifetimeScope,而hubContextIHubContext<MyHub, IMyHub>。变量provider是注入到控制器构造函数中的IPrincipalProvider

问题在于,在任务中尝试解析IMyService时,它会创建IPrincipalProvider的一个实例,而该实例要求的IHttpContextAccessor不再存在。

这种情况下的解决方案是什么?我需要在服务上有第二个构造函数,使用不同的IPrincipalProvider,从其他位置获取上下文吗?如果是这样,那从哪里获取呢?


2
HttpContext 在请求开始时创建,在响应结束时销毁。 后台任务独立于 HttpContext 运行,因此在其中获取 HttpContext 是不可行的。 - Eldar
3
不要在后台服务中使用IHttpContextAccessor ;) - Fabio
是的,我知道。那么有什么替代方案吗?我需要一种替代方案,使我能够使用Resolve将主要提供程序(或其变体)注入到服务的ctor中。 - Ivan-Mark Debono
1
你是否无法在后台运行的函数之外解决IMyService?这样,当HttpContextAccessor存在且UserPrinciple保存在实例中时,就可以解决IMyService了。然后你只需要编写var myService = theResolvedService即可。 - Darem
@Darm 我尝试通过将 ILifetimeScope 注入到构造函数中,然后创建一个子作用域,并在其中包装任务来解决这个问题。但是问题仍然存在,因为 DbContext 也被释放了。 - Ivan-Mark Debono
你需要从HTTP上下文中提取数据并将其传递给后台服务。IHttpContextAccessor本身是一个单例,因此它存在于应用程序的整个生命周期中。但它从请求Task的环境数据中读取HttpContext。请求完成后,上下文也会消失。 - weichch
1个回答

0

最好的解决方案是有两个IPrincipalProvider的实现,一个使用httpContextAccessor,另一个使用其他东西。不幸的是,拥有另一个实现并不总是容易的。

当您创建子生命周期范围时,可以向该子生命周期范围添加注册。您可以在此处注册StaticPrincipalProvider

 private async Task BackgroundProcessing(...) {
    ... 
    try {
        using(ILifetimeScope queueScope = this._rootScope.BeginLifetimeScope(builder => {
            builder.RegisterInstance(new StaticPrincipalProvider(principal))
                   .As<IPrincipalProvider>();
        })){
            await workItem(queueScope, stoppingToken);
        }
    }
    ...
 }

现在你需要做的就是找到一种方法,在出队任务时获取相应的主体。为此,您可以更改BackgroundTaskQueue的实现,使用ConcurrentQueue<WorkItem>代替ConcurrentQueue<Func<ILifetimeScope, CancellationToken, Task>>,其中WorkItem
public class WorkItem {
    public Func<ILifetimeScope, CancellationToken, Task> Work { get; private set; }
    public IPrincipal Principal { get; private set; }
    // or
    public Action<ContainerBuilder> builderAccessor { get; private set; }
}

由于BackgroundTaskQueue是使用请求范围实例化的,因此您将可以访问当前主体。


工作项如何使用queueScope实例化? - Ivan-Mark Debono
我已经将当前代码添加到Github上:https://github.com/cryo75/BackgroundQueue - Ivan-Mark Debono
因为它是一个数据对象而不是服务,所以您可以使用C#new关键字实例化它。有很多地方可以创建它,例如在BackgroundTaskQueueQueueBackgroundWorkItem方法中。 - Cyril Durand
@Ivan-MarkDebono 抱歉,那是一次错误的复制/粘贴。该函数仍应具有生命周期范围。 - Cyril Durand
我已经让它在某种程度上工作了。然而,问题出在作用域上,因为它使用的是控制器构造函数的作用域,而不是子作用域。 - Ivan-Mark Debono
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接