取消令牌注入

16

我想通过依赖注入而不是每次作为参数传递取消标记。这可行吗?

我们有一个asp.net-core 2.1应用程序,其中我们将来自控制器的调用传递到一堆异步库、处理程序和其他服务中,以满足我们服务的金融监管领域的拜占庭需求。

在请求的顶部,我可以声明我需要一个取消标记,然后我会得到一个:

[HttpPost]
public async Task<IActionResult> DoSomeComplexThingAsync(object thing, CancellationToken cancellationToken) {
    await _someComplexLibrary.DoThisComplexThingAsync(thing, cancellationToken);
    return Ok();
}

现在,我想成为一名优秀的异步编程人员,并确保我的cancellationToken通过调用链传递到每个异步方法。我想确保它被传递给EF、System.IO流等。我们有所有你期望的常规存储库模式和消息传递实践。我们试图保持我们的方法简洁并具有单一职责。我的技术领导听到"Fowler"这个词就会兴奋不已。因此,我们的类大小和函数体很小,但我们的调用链非常深。
这意味着每一层、每一个函数都必须交接这个该死的令牌:
private readonly ISomething _something;
private readonly IRepository<WeirdType> _repository;

public SomeMessageHandler(ISomething<SomethingElse> something, IRepository<WeirdType> repository) {
    _something = something;
    _repository = repository;
}

public async Task<SomethingResult> Handle(ComplexThing request, CancellationToken cancellationToken) {
    var result = await DoMyPart(cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    result.SomethingResult = await _something.DoSomethingElse(result, cancellationToken);
    return result;
}

public async Task<SomethingResult> DoMyPart(ComplexSubThing request, CancellationToken cancellationToken) {
    return await _repository.SomeEntityFrameworkThingEventually(request, cancellationToken);
}

这种情况会无限循环,根据我们领域复杂性的需要。似乎CancellationToken在我们的代码库中出现的次数比任何其他术语都要多。尽管我们声明了数百万个对象类型,但我们的参数列表通常已经太长了(即超过一个)。现在我们还有这个额外的小取消令牌伙伴在每个参数列表和每个方法声明中挂着。

我的问题是,既然Kestrel和/或管道首先给了我这个令牌,如果我能像这样做就太好了:

private readonly ISomething _something;
private readonly IRepository<WeirdType> _repository;
private readonly ICancellationToken _cancellationToken;

public SomeMessageHandler(ISomething<SomethingElse> something, ICancellationToken cancellationToken)
{
    _something = something;
    _repository = repository;
    _cancellationToken = cancellationToken;
}

public async Task<SomethingResult> Handle(ComplexThing request)
{
    var result = await DoMyPart(request);
    _cancellationToken.ThrowIfCancellationRequested();
    result.SomethingResult = await _something.DoSomethingElse(result);
    return result;
}

public async Task<SomethingResult> DoMyPart(ComplexSubThing request)
{
    return await _repository.SomeEntityFrameworkThingEventually(request);
}

然后通过 DI 组合将其传递,当我有需要显式令牌的东西时,我可以这样做:

private readonly IDatabaseContext _context;
private readonly ICancellationToken _cancellationToken;

public IDatabaseRepository(IDatabaseContext context, ICancellationToken cancellationToken)
{
    _context = context;
    _cancellationToken = cancellationToken;
}

public async Task<SomethingResult> DoDatabaseThing()
{
    return await _context.EntityFrameworkThing(_cancellationToken);
}

我是疯了吗?每次都传递该死的令牌,并赞美异步神明所赐予的恩惠?我应该重新培训成为骆驼农民吗?它们看起来很不错。甚至询问这个问题是否有些异端邪说?我现在应该忏悔吗?我认为为了使async/await正常工作,令牌必须在函数声明中。所以,也许就是骆驼。


1
你的问题是如何在调用操作之前捕获该令牌?不太确定你在实现中卡住了哪里...(如果你想讨论这是否对你的情况是一个好主意,SO不是正确的地方,可能在https://softwareengineering.stackexchange.com/上讨论该设计可能是有关话题的) - Alexei Levenkov
1
这个CancellationToken的重复是我在编写可取消代码时也考虑过很多次的事情。据我所知,它源自于.NET 4,当时取消概念被添加到BCL中,远在DI成为问题之前。您可以通过提交问题或讨论,在https://github.com/dotnet/runtime上获得更好的响应。此外,羊驼会对您的异端行为进行唾弃,因此养殖可能不可取。 - Ian Kemp
4
不止一个取消标记。可能有任意数量的标记,它们可能相关也可能不相关。依赖注入容器如何可能找出要使用哪个标记?你如何可能实际取消由依赖注入容器提供的标记?因此,注入取消标记并没有太多意义,因为取消标记通常与特定的调用有关,而不是对象(及其生命周期)。 - poke
1
话虽如此,您应该考虑一下您的系统中是否真的所有内容都需要取消令牌。如果有一些东西实际上是不可取消的,那么就不应该允许通过取消令牌来取消它。例如,您上面的 SomeMessageHandler.Handle 是*不可取消的。在完成所有工作后返回结果之前检查令牌并不是取消的意义所在。取消是指实际停止工作而不仅仅是中断调用流程。 - poke
1
您可以查看此答案,了解如何通过IHttpAccessorCancellationToken注入到服务中。 - Métoule
显示剩余4条评论
5个回答

6
我觉得你的想法很棒,我不认为你需要后悔或忏悔。 这是一个很好的主意,我也考虑过,并且实施了自己的解决方案。
public abstract class RequestCancellationBase
{
    public abstract CancellationToken Token { get; }

    public static implicit operator CancellationToken(RequestCancellationBase requestCancellation) =>
        requestCancellation.Token;
}

public class RequestCancellation : RequestCancellationBase
{
    private readonly IHttpContextAccessor _context;

    public RequestCancellation(IHttpContextAccessor context)
    {
        _context = context;
    }

    public override CancellationToken Token => _context.HttpContext.RequestAborted;
}

注册应该是这样的

services.AddHttpContextAccessor();
services.AddScoped<RequestCancellationBase, RequestCancellation>();

现在你可以在任何地方注入RequestCancellationBase,更好的是你可以直接将它传递给每个期望CancellationToken的方法。这是因为有了public static implicit operator CancellationToken(RequestCancellationBase requestCancellation)
下面是一个使用示例。
public sealed class Service1
{
    private readonly RequestCancellationBase _cancellationToken;
    
    public Service1(RequestCancellationBase cancellationToken)
    {
         _cancellationToken = cancellationToken;
    }

    public async Task SomeMethod()
    {
        HttpClient client = new();
        // passing RequestCancellationBase object instead of passing CancellationToken
        // without any casting
        await client.GetAsync("url", _cancellationToken);
    }
}

这个解决方案对我很有帮助,希望对你也有帮助。

非常好的答案!谢谢! - Gerardo Buenrostro González
1
这是一个非常好的针对作用域服务的想法,但是对于单例服务,我会持谨慎态度 - 单例服务可以获取HttpContext的令牌,但是在服务的生命周期内将有多个上下文和因此令牌 - 您必须了解令牌的实现。我认为令牌需要与事物具有相同的生命周期 - 因此在类上具有上下文范围,但对于单例服务,则将其传递给方法。 - Keith
自从我升级到.NET 6后,这个程序不再适用于我了,我不知道为什么,它以前运行得非常完美。 - Gerardo Buenrostro González

3
首先,有三种依赖注入作用域:Singleton、Scoped和Transient。其中两种排除了使用共享的令牌。
使用AddSingleton添加的DI服务存在于所有请求中,因此任何取消令牌都必须传递到特定的方法(或整个应用程序)。
使用AddTransient添加的DI服务可能会按需实例化,您可能会遇到一个问题,即为已经取消的令牌创建了新的实例。它们可能需要一些方式将当前令牌传递给[FromServices]或其他库更改。
但是,对于AddScoped,我认为有一种方法,并且我得到了这个回答中提到类似问题的帮助 - 您不能将令牌本身传递给DI,但可以传递IHttpContextAccessor
因此,在Startup.ConfigureServices或您用于注册任何IRepository的扩展方法中:

// For imaginary repository that looks something like
class RepositoryImplementation : IRepository {
    public RepositoryImplementation(string connection, CancellationToken cancellationToken) { }
}

// Add a scoped service that references IHttpContextAccessor on create
services.AddScoped<IRepository>(provider => 
    new RepositoryImplementation(
        "Repository connection string/options",
        provider.GetService<IHttpContextAccessor>()?.HttpContext?.RequestAborted ?? default))

IHttpContextAccessor服务将在每个HTTP请求中检索一次,?.HttpContext?.RequestAborted将返回与在控制器操作内部调用this.HttpContext.RequestAborted或将其添加到操作参数相同的CancellationToken。


3
这是我如何将HttpContext的取消令牌注入到我的存储库中。

WebApi/Program.cs

builder.Services.AddScoped<ICancellationTokenService, CurrentCancellationTokenService>();

WebApi/Services/CurrentCancellationTokenService.cs

public class CurrentCancellationTokenService : ICancellationTokenService
{
    public CancellationToken CancellationToken { get; }

    public CurrentCancellationTokenService(IHttpContextAccessor httpContextAccessor)
    {
        CancellationToken = httpContextAccessor.HttpContext!.RequestAborted;
    }
}

核心/领域/接口/ICancellationTokenService
public class ICancellationTokenService
{
    public CancellationToken CancellationToken { get; }
}

Persistence/Dal/Repositories/ClientRepository.cs

public class ClientRepository : IClientRepository
{
    private readonly CancellationToken _cancellationToken;

    public ClientRepository(ICancellationTokenService cancellationTokenService, ...)
    {
        _cancellationToken = cancellationTokenService.CancellationToken;
    }

    public Task<Client?> GetByIdAsync(int clientId)
    {
        return _context.Client.FirstOrDefaultAsync(x => x.Id == clientId, _cancellationToken);
    }
}

0

我知道这是一个两年前的帖子,但因为我也带着同样的问题来到这里,所以提供一个直接令牌交付的解决方案:

    var services = new ServiceCollection();
    services.AddSingleton<CancellationTokenSource>();
    services.AddSingleton(typeof(CancellationToken),
        (IServiceProvider sp) =>
        {
            var cts = sp.GetService<CancellationTokenSource>();
            ArgumentNullException.ThrowIfNull(cts);
            return cts.Token;
        }
        );

没有任何东西可以取消那个令牌,所以你可以直接返回default。最好使用RequestAborted,因为如果浏览器中止请求,它将取消令牌。 - undefined
明白了,谢谢 @AaronQueenan - undefined

0
只需使用:
services.AddHttpContextAccessor();
services.AddScoped(typeof(CancellationToken),
    serviceProvider => serviceProvider.GetRequiredService<IHttpContextAccessor>()
        .HttpContext!.RequestAborted);

请注意使用RequestAborted。如果浏览器中止请求,这将取消令牌。

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