Inyección de token de cancelación
Me gustaría poder pasar tokens de cancelación mediante inyección de dependencia en lugar de como parámetros cada vez. ¿Es esto una cosa?
Tenemos una aplicación asp.net-core 2.1, donde pasamos las llamadas de los controladores a un laberinto de bibliotecas asíncronas, controladores y otros servicios para satisfacer las necesidades bizantinas del dominio regulador de fintech al que damos servicio.
En la parte superior de la solicitud, puedo declarar que quiero un token de cancelación y obtendré uno:
[HttpPost]
public async Task<IActionResult> DoSomeComplexThingAsync(object thing, CancellationToken cancellationToken) {
await _someComplexLibrary.DoThisComplexThingAsync(thing, cancellationToken);
return Ok();
}
Ahora, quiero ser un buen programador asíncrono y asegurarme de que mi cancellationTokenpase a todos los métodos asíncronos a lo largo de la cadena de llamadas. Quiero asegurarme de que se pase a EF, flujos System.IO, etc. Tenemos todos los patrones de repositorio habituales y las prácticas de paso de mensajes que esperarías. Intentamos que nuestros métodos sean concisos y tenemos una única responsabilidad. Mi líder tecnológico se excita visiblemente con la palabra 'Fowler'. Entonces, el tamaño de nuestras clases y los cuerpos funcionales son pequeños, pero nuestras cadenas de llamadas son muy, muy profundas.
Lo que esto significa es que cada capa, cada función, tiene que entregar el maldito 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);
}
Esto continúa ad infinitum, según las necesidades de la complejidad de nuestro dominio. Parece que CancellationTokenaparece más veces en nuestro código base que cualquier otro término. Nuestras listas de arg a menudo ya son demasiado largas (es decir, más de una) tal como están, aunque declaramos un millón de tipos de objetos. Y ahora tenemos este pequeño compañero de token de cancelación adicional en cada lista de argumentos, en cada método de rechazo.
Mi pregunta es, dado que Kestrel y / o la canalización me dieron el token en primer lugar, sería genial si pudiera tener algo como esto:
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);
}
Esto luego se pasaría a través de la composición DI, y cuando tuviera algo que necesite el token explícitamente, podría hacer esto:
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);
}
¿Estoy loco? ¿Solo paso la maldita ficha, cada maldita vez, y alabo a los dioses asíncronos por la recompensa que se me ha dado? ¿Debería volver a capacitarme como granjero de llamas? Parecen agradables. ¿Incluso preguntar esto es una especie de herejía? ¿Debería arrepentirme ahora? Creo que para que async / await funcione correctamente, el token debe estar en func decl. Entonces, tal vez llamas
Respuestas
En primer lugar, hay 3 ámbitos de inyección: Singleton, Scoped y Transient. Dos de ellos descartan el uso de un token compartido.
Los servicios DI agregados con AddSingletonexisten en todas las solicitudes, por lo que cualquier token de cancelación debe pasarse al método específico (o en toda la aplicación).
Los servicios DI agregados con AddTransientse pueden crear instancias bajo demanda y es posible que tenga problemas cuando se crea una nueva instancia para un token que ya está cancelado. Probablemente necesitarían alguna forma de pasar el token actual [FromServices]o algún otro cambio de biblioteca.
Sin embargo, AddScopedcreo que hay una manera, y esta respuesta a mi pregunta similar me ayudó : no se puede pasar el token a DI, pero se puede pasar IHttpContextAccessor.
Entonces, en Startup.ConfigureServiceso el método de extensión que usa para registrar cualquier IRepositoryuso:
// 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))
Ese IHttpContextAccessorservicio se recuperará una vez por solicitud HTTP, y ?.HttpContext?.RequestAborteddevolverá lo mismo CancellationTokenque si hubiera llamado this.HttpContext.RequestAborteddesde dentro de una acción de controlador o lo hubiera agregado a los parámetros de la acción.