Estoy creando un cliente para consumir continuamente nuevos registros de una fuente de datos. La integración se basa en extracción y mi cliente consulta periódicamente la fuente de datos en busca de nuevos registros. Estoy usando IAsyncEnumerable como tipo de devolución para este flujo continuo de nuevos registros. Aquí está la esencia del método en cuestión:
public async IAsyncEnumerable<Record> StreamRecords(...) { while (!cancellationToken.IsCancellationRequested) { using var response = await _httpClient.SendAsync(request, cancellationToken); var records = _parser.Parse(response.Content); foreach (var r in records) yield return r; await Task.Delay(waitPeriod, cancellationToken); } }¿Es este un uso apropiado de IAsyncEnumerable? Esta secuencia debe ser "interminable" o "continua" (al menos hasta el token de cancelación o el error).
Como con la mayoría de las cosas: el punto clave es que la intención y el comportamiento se comuniquen claramente. No veo ningún problema conceptual sobre una secuencia sin fin, ya sea IEnumerable<T> o IAsyncEnumerable<T> , pero obviamente si el consumidor llama a ToList[Async]() , las cosas terminarán mal. Esto no es un problema en sí mismo, para dar un escenario similar: algunas secuencias I[Async]Enumerable<T> no son repetibles , ya sea que solo se pueden iterar una vez (con intentos posteriores fallidos), o podrían producir resultados diferentes cada uno tiempo (sin siquiera requerir cosas como cambios en la lista). Esto es perfectamente legal, pero existe un código que asume que las secuencias son repetibles y producirán los mismos datos . La misma discusión existe aquí y, en última instancia, no es culpa del productor , sino del consumidor .
Entonces: su secuencia (productor) funcionará perfectamente bien con un consumidor razonable que entienda que los datos deben tratarse como ilimitados. Si eso es lo que tiene su aplicación, entonces: ¡genial!
Su método StreamRecords devuelve esencialmente una secuencia de consumo, de naturaleza similar a las secuencias devueltas por los métodos BlockingCollection<T>.GetConsumingEnumerable y ChannelReader<T>.ReadAllAsync . Consumir significa que cuando la persona que llama enumera la secuencia, los elementos devueltos se eliminan permanentemente de algún almacenamiento de respaldo. En el caso de estos dos métodos, el almacenamiento de respaldo es un ConcurrentQueue<T> interno. En su caso (según este comentario), el almacenamiento de respaldo se encuentra en el lado del servidor, con algún código del lado del cliente que sabe qué datos buscar a continuación.
Exponer una secuencia de consumo conlleva algunos desafíos:
break o return deliberadamente del ciclo await foreach , o involuntariamente por una excepción transitoria lanzada dentro del ciclo? ¿Hay algún elemento consumido en peligro de perderse? Con respecto al 1er desafío, puede leer este problema de GitHub, o mejor aún, ver este video , donde se debatieron las diversas opciones. Alerta de spoiler, los ingenieros de Microsoft se conformaron con el aparentemente inocuo ReadAllAsync .
Con respecto al segundo desafío, puede leer esta pregunta, que muestra que la decisión (técnicamente justificada) tomada por Microsoft con respecto a la API ChannelReader<T>.ReadAllAsync resultó en un comportamiento inesperado/no intuitivo.
Con respecto al tercer desafío, podría considerar aprovechar la mecánica de los bloques finally dentro de los métodos del iterador. Mira esta respuesta para más detalles.
Debido a estos matices, podría ser una buena idea brindar opciones de consumo adicionales a las personas que llaman. Algo así como public Task<Record[]> TakeAllNewRecords() por ejemplo.