.NET Core 托管服务需要 HttpContext。

8

编辑摘要

我有一个后台服务,需要DBContext。我的DbContext依赖于HttPContext,因为它使用UserClaims来过滤上下文。但是,在从BackgroundService请求DbContext时,HttpContext为空(这是出于设计考虑,因为BackgroundService没有关联的WebRequest)。

如何捕获和模拟HTTPContext,以便我的用户声明可以传递到后台服务?

我正在编写一个托管服务,将在Signalr中推送信息。我的Background。

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem);
    Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<Func<IServiceProvider, CancellationToken, Task>> _workItems = new ConcurrentQueue<Func<IServiceProvider, CancellationToken, Task>>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(Func<IServiceProvider, CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        this._workItems.Enqueue(workItem);
        this._signal.Release();
    }

    public async Task<Func<IServiceProvider, CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        await this._signal.WaitAsync(cancellationToken);
        this._workItems.TryDequeue(out var workItem);

        return workItem;
    }
}

我正在排队处理需要使用 DbContext 的工作项,我的 DbContext 会根据已登录的用户自动过滤数据,因此需要访问 HTTPContextAccessor。

当我排队处理需要使用 DbContext 的项目时,出现了 HTTPContext 为空的错误,这是有道理的,因为我正在后台进程中运行。

var backgroundTask = sp.GetRequiredService<IBackgroundTaskQueue>();

backgroundTask.QueueBackgroundWorkItem((isp, ct) => {
    //When this line is executed on the background task it throws
    var context = sp.GetService<CustomDbContext>(); 
    //... Do work with context
});

我的DBContext使用HTTPContextAccessor来过滤数据:

public CustomDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor)
{
}

有没有办法在每个任务中捕获HTTPContext,或者Moq它并捕获UserClaims,并将它们替换为BackgroundService的范围内?

我如何从我的后台服务中使用依赖于HTTPContext的服务。


在创建任务时,为什么不坚持用户原则呢? - Nkosi
@Nkosi,我的dbContext接受httpcontextaccessor作为参数,在该范围内如何替换httpcontextaccessor? - johnny 5
啊,我不知道那个。 (假设是为了审计或其他什么?)你能包含那个小细节吗?应该有助于我看到更大的画面。 - Nkosi
我现在会进行编辑。 - johnny 5
我建议模拟 DbContext 本身,而不是模拟 HttpRequest 然后使用真正的 DbContext 类。在单元测试时,我认为没有必要让一个真正的 dbContext 对象接触数据库。你可以在这里看到一个例子:https://dev59.com/aV8e5IYBdhLWcg3wNYUn - Rey
显示剩余3条评论
2个回答

7
我的DbContext依赖于HttpContext,因为它使用UserClaims来过滤上下文。
这听起来已经是一个糟糕的设计。你的数据库上下文应该只关心提供数据库访问。我认为基于用户权限进行过滤已经是对数据库上下文的责任过重了,你应该将其移动到另一个层次中。但即便如此,在计划从执行超出HTTP上下文的地方使用时,你也应该尽量避免依赖于HTTP上下文。相反地,考虑显式地将用户或用户声明传递给被调用的方法。这样一来,你就不会引入隐藏在上下文中的用户的隐式依赖性。
至于模拟上下文,你实际上不需要使用像Moq这样的模拟库来模拟上下文。你可以简单地创建一个DefaultHttpContext并设置其中的用户,例如:
var context = new DefaultHttpContext()
{
    User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
    {
        new Claim(ClaimTypes.Name, "Foo"),
    })),
};

但是,仅仅能够创建HTTP上下文并不能帮助您向数据库上下文提供上下文:一旦服务提供程序被创建,您便无法替换已注册的服务。因此,您必须在HttpContextAccessor上设置上下文,但我强烈建议不要这样做,因为在后台服务中使用可能不安全;或者显式地创建数据库上下文,但我也建议不要这样做,因为这将使DI失去作用。
所以,“正确”的方法可能是重新考虑架构,使您不需要依赖于HTTP上下文,最理想的情况甚至不需要依赖于用户。毕竟,后台服务不在HTTP上下文的范围内运行,因此它也不在单个用户的范围内运行。
另外,请记住,数据库上下文是有作用域的依赖项,因此您不应在依赖项范围之外解析它们。如果您需要在后台服务中使用数据库上下文,应首先使用IServiceScopeFactory显式地打开一个依赖项范围。

1
在这个例子中,我设计了糟糕的架构,但假设有一个需要从后台服务获取用户特定依赖的示例。从你的回答中,我得出的结论是,我应该将我的依赖最小化为自定义对象,而不是使用HttpContextAccessor。例如,ContextUserClaims,并在请求之前从服务容器中重写该实现? - johnny 5
如果您具有用户特定上下文,则显然应从该用户特定上下文与后台服务通信(例如通过从控制器调用后台服务的方法或发送一些消息),那么您应在此处收集用户特定依赖项并将其明确传递给后台服务。例如,_bgService.DoSomethingForUser(new SomethingContext { UserId = User.GetUserId() })。因此,您的后台服务不会全局依赖它,而是某些个别方法或消息需要传递该信息。 - poke
好的,我会将其标记为正确,并稍后发布一个答案,说明我是如何具体解决这个问题的。 - johnny 5

0

你没有分享你的配置,所以请原谅我,我要在这里猜测一下...

由于某些依赖注入问题,你可能会得到一个null值的IHttpContextAccessor

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); // this
}

实际上,我相信你也可以通过以下方式实现完全相同的事情

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddHttpContextAccessor(); // does pretty much the same as above
}

该扩展的源代码在这里


2
抱歉,我应该更清楚地表达。问题在于后台服务没有HttpContext,因为没有发出Web请求。这是.Net-Core的一个新功能,允许您在后台运行任务。如果我有需要HTTP上下文的依赖项,我想知道如何解决这个问题。 - johnny 5
如果IHttpContextAccessor没有注册,通常在对象构造期间会出现无法解析类型的异常。使用依赖注入时,您不会得到所需依赖项的null值。 - poke

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