简体   繁体   English

取消代币注入

[英]Cancellation Token Injection

I'd like to be able to pass cancellation tokens via dependency injection instead of as parameters every time.我希望能够通过依赖注入而不是每次都作为参数传递取消令牌。 Is this a thing?这是一回事吗?

We have an asp.net-core 2.1 app, where we pass calls from controllers into a maze of async libraries, handlers and other services to fulfil the byzantine needs of the fintech regulatory domain we service.我们有一个 asp.net-core 2.1 应用程序,我们将来自控制器的调用传递到异步库、处理程序和其他服务的迷宫中,以满足我们服务的金融科技监管领域的拜占庭需求。

At the top of the request, I can declare that I want a cancellation token, and I'll get one:在请求的顶部,我可以声明我想要一个取消令牌,我会得到一个:

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

Now, I want to be a good async programmer and make sure my cancellationToken gets passed to every async method down through the call chain.现在,我想成为一名优秀的异步程序员,并确保我的cancellationToken令牌通过调用链传递给每个异步方法。 I want to make sure it gets passed to EF, System.IO streams, etc. We have all the usual repository patterns and message passing practices you'd expect.我想确保它被传递给 EF、System.IO 流等。我们拥有您期望的所有常用存储库模式和消息传递实践。 We try to keep our methods concise and have a single responsibility.我们尽量保持我们的方法简洁,并有一个单一的责任。 My tech lead gets visibly aroused by the word 'Fowler'.我的技术负责人显然被“福勒”这个词激怒了。 So our class sizes and function bodies are small, but our call chains are very, very deep.所以我们的类大小和函数体很小,但我们的调用链非常非常深。

What this comes to mean is that every layer, every function, has to hand off the damn token:这意味着每一层、每一个功能都必须交出该死的令牌:

    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);
    }

This goes on ad infinitum, as per the needs of our domain complexity.根据我们域复杂性的需要,这无休止地进行下去。 It seems like CancellationToken appears more times in our codebase than any other term.看起来CancellationToken在我们的代码库中出现的次数比任何其他术语都要多。 Our arg lists are often already too long (ie more than one) as it is, even though we declare a million object types.我们的 arg 列表通常已经太长(即不止一个),即使我们声明了一百万个对象类型。 And now we have this extra little cancellation token buddy hanging around in every arg list, every method decl.现在我们在每个 arg 列表和每个方法 decl 中都有这个额外的小取消令牌伙伴。

My question is, since Kestrel and/or the pipeline gave me the token in the first place, it'd be great if I could just have something like this:我的问题是,由于 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);
    }

This would then get passed around via DI composition, and when I had something that needs the token explicitly I could do this:然后这将通过 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);
    }

Am I nuts?我疯了吗? Do I just pass the damn token, every damn time, and praise the async gods for the bounty that has been given?我是不是每时每刻都传递该死的令牌,并为所给予的赏金赞美异步众神? Should I just retrain as a llama farmer?我应该重新培训成为美洲驼农民吗? They seem nice.他们看起来不错。 Is even asking this some kind of heresy?甚至问这是某种异端邪说吗? Should I be repenting now?我现在应该忏悔吗? I think for async/await to work properly, the token has to be in the func decl.我认为要使 async/await 正常工作,令牌必须在 func decl 中。 So, maybe llamas it is所以,也许是骆驼

I think you are thinking in a great way, I do not think you need to regret or repent, this is a great idea, I thought also about it, and I come to a solution我认为你的想法很好,我认为你不需要后悔或忏悔,这是个好主意,我也考虑过,我得出了解决方案

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;
}

and the registration should be like this并且注册应该是这样的

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<RequestCancellationBase, RequestCancellation>();

now you can inject RequestCancellationBase wherever you want,现在你可以在任何你想要的地方注入RequestCancellationBase
and the better thing is that you can directly pass it to every method that expects CancellationToken this is because of public static implicit operator CancellationToken(RequestCancellationBase requestCancellation)更好的是,您可以将它直接传递给每个需要CancellationToken方法,这是因为public static implicit operator CancellationToken(RequestCancellationBase requestCancellation)

this solution helped me, hope it is helpful for you also这个解决方案帮助了我,希望它也对你有帮助

First of all, there are 3 injection scopes: Singleton, Scoped and Transient.首先,有 3 个注入作用域:Singleton、Scoped 和 Transient。 Two of those rule out using a shared token.其中两个排除使用共享令牌。

DI services added with AddSingleton exist across all requests, so any cancellation token must be passed to the specific method (or across your entire application).使用AddSingleton添加的 DI 服务存在于所有请求中,因此任何取消令牌都必须传递给特定方法(或跨整个应用程序)。

DI services added with AddTransient may be instantiated on demand and you may get issues where a new instance is created for a token that is already cancelled.使用AddTransient添加的 DI 服务可能会按需实例化,并且您可能会遇到为已取消的令牌创建新实例的问题。 They'd probably need some way for the current token to be passed to [FromServices] or some other library change.他们可能需要某种方式将当前令牌传递给[FromServices]或其他一些库更改。

However, for AddScoped I think there is a way, and I was helped by this answer to my similar question - you can't pass the token itself to DI, but you can pass IHttpContextAccessor .但是,对于AddScoped我认为有一种方法,我对类似问题这个答案帮助了- 您不能将令牌本身传递给 DI,但可以传递IHttpContextAccessor

So, in Startup.ConfigureServices or the extension method you use to register whatever IRepository use:因此,在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))

That IHttpContextAccessor service will be retrieved once per HTTP request, and that ?.HttpContext?.RequestAborted will return the same CancellationToken as if you had called this.HttpContext.RequestAborted from inside a controller action or added it to the parameters on the action. IHttpContextAccessor服务将在每个 HTTP 请求中检索一次,而?.HttpContext?.RequestAborted将返回相同的CancellationToken ,就像您从控制器操作内部调用this.HttpContext.RequestAborted或将其添加到操作的参数一样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM