Injection de jeton d'annulation

Sep 30 2020

J'aimerais pouvoir transmettre des jetons d'annulation via l'injection de dépendances au lieu de paramètres à chaque fois. Est-ce une chose?

Nous avons une application asp.net-core 2.1, où nous passons les appels des contrôleurs dans un labyrinthe de bibliothèques asynchrones, de gestionnaires et d'autres services pour répondre aux besoins byzantins du domaine de réglementation de la fintech que nous desservons.

En haut de la demande, je peux déclarer que je veux un jeton d'annulation, et j'en obtiendrai un:

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

Maintenant, je veux être un bon programmeur asynchrone et m'assurer que mon cancellationTokenest transmis à chaque méthode asynchrone tout au long de la chaîne d'appels. Je veux m'assurer qu'il est transmis aux flux EF, System.IO, etc. Nous avons tous les modèles de référentiel habituels et les pratiques de transmission de messages que vous attendez. Nous essayons de garder nos méthodes concises et d'avoir une seule responsabilité. Mon responsable technique est visiblement excité par le mot «Fowler». Ainsi, la taille de nos classes et les corps de fonctions sont petits, mais nos chaînes d'appels sont très, très profondes.

Cela signifie que chaque couche, chaque fonction doit transmettre le foutu jeton:

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

Cela continue à l'infini, selon les besoins de la complexité de notre domaine. Il semble CancellationTokenapparaître plus de fois dans notre base de code que tout autre terme. Nos listes d'arguments sont souvent déjà trop longues (c'est-à-dire plus d'une) telles quelles, même si nous déclarons un million de types d'objets. Et maintenant, nous avons ce petit copain de jeton d'annulation supplémentaire qui traîne dans chaque liste d'arguments, chaque méthode déclinée.

Ma question est, puisque Kestrel et / ou le pipeline m'ont donné le jeton en premier lieu, ce serait génial si je pouvais simplement avoir quelque chose comme ceci:

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

Cela serait ensuite transmis via la composition DI, et quand j'avais quelque chose qui nécessite explicitement le jeton, je pourrais le faire:

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

Suis-je fou? Dois-je simplement passer le putain de jeton, à chaque putain de temps, et féliciter les dieux asynchrones pour la prime qui a été donnée? Dois-je simplement me recycler comme éleveur de lamas? Ils semblent gentils. Est-ce même demander à cela une sorte d'hérésie? Dois-je me repentir maintenant? Je pense que pour que async / await fonctionne correctement, le jeton doit être dans la fonction décl. Alors, peut-être que les lamas sont

Réponses

1 Keith Oct 06 2020 at 04:19

Tout d'abord, il existe 3 portées d'injection: Singleton, Scoped et Transient. Deux d'entre eux excluent l'utilisation d'un jeton partagé.

Les services DI ajoutés avec AddSingletonexistent dans toutes les demandes, donc tout jeton d'annulation doit être transmis à la méthode spécifique (ou à l'ensemble de votre application).

Les services DI ajoutés avec AddTransientpeuvent être instanciés à la demande et vous pouvez rencontrer des problèmes où une nouvelle instance est créée pour un jeton qui est déjà annulé. Ils auraient probablement besoin d'un moyen pour que le jeton actuel soit transmis [FromServices]ou pour un autre changement de bibliothèque.

Cependant, car AddScopedje pense qu'il existe un moyen, et j'ai été aidé par cette réponse à ma question similaire - vous ne pouvez pas passer le jeton lui-même à DI, mais vous pouvez le passer IHttpContextAccessor.

Donc, dans Startup.ConfigureServicesou la méthode d'extension que vous utilisez pour enregistrer toute IRepositoryutilisation:


// 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))

Ce IHttpContextAccessorservice sera récupéré une fois par requête HTTP, et cela ?.HttpContext?.RequestAbortedretournera le même CancellationTokenque si vous aviez appelé this.HttpContext.RequestAborteddepuis l'intérieur d'une action de contrôleur ou l' avez ajouté aux paramètres de l'action.