No meu artigo anterior , descrevi como obter o comportamento da mônada "Talvez" usando operadores async / wait . Desta vez, vou mostrar como implementar outro padrão de design popular "Reader Monad" usando as mesmas técnicas.
Esse padrão permite a passagem implícita de algum contexto para alguma função sem o uso de parâmetros de função ou objetos globais compartilhados e pode ser considerado como mais uma maneira de implementar a injeção de dependência. Por exemplo:
class Config { public string Template; } public static async Task Main() { Console.WriteLine(await GreetGuys().Apply(new Config {Template = "Hi, {0}!"}));
"Leitor" clássico
Primeiro, vamos dar uma olhada em como a mônada pode ser implementada sem operadores assíncronos / aguardados :
public class Config { public string Template; } public static class ClassicReader { public static void Main() { var greeter = GreetGuys(); Console.WriteLine(greeter.Apply(new Config{Template = "Hello, {0}"}));
(Leitor)
public class Reader<T, TCtx> { private readonly Func<TCtx, T> _exec; public Reader(Func<TCtx, T> exec) => this._exec = exec; public T Apply(TCtx ctx) => this._exec(ctx); } public static class Reader { public static Reader<TJoin, TCtx> SelectMany<TIn, TOut, TCtx, TJoin>( this Reader<TIn, TCtx> source, Func<TIn, Reader<TOut, TCtx>> bind, Func<TIn, TOut, TJoin> join) => new Reader<TJoin, TCtx>(ctx => { var inValue = source.Apply(ctx); var outValue = bind(inValue).Apply(ctx); return join(inValue, outValue); }); }
O código funciona, mas não parece natural para desenvolvedores de C #. Não é de admirar, porque as mônadas vieram de linguagens funcionais, onde um código semelhante pode ser escrito de uma maneira mais concisa. No entanto, a implementação clássica ajuda a subestimar a essência do padrão - em vez da execução imediata de algum código, ele é colocado em uma função que será chamada quando seu contexto estiver pronto.
public static Reader<string, Config> Greet(string name) => new Reader<string, Config>(cfg => string.Format(cfg.Template, name));
O SelectMany pode combinar várias dessas funções em uma única, para que você possa criar uma sub-rotina cuja execução será adiada até que seu contexto seja aplicado. Por outro lado, essa abordagem se assemelha à escrita de código assíncrono em que a execução do programa é interrompida se alguma operação assíncrona estiver em execução. Quando um resultado da operação estiver pronto, a execução do programa continuará. Supõe-se que a infraestrutura C # projetada para trabalhar com operações assíncronas ( assíncrona / aguardar ) possa ser de alguma forma utilizada na implementação da mônada "Reader" e ... a suposição está correta!
"Leitor" assíncrono
No meu artigo anterior , demonstrei como obter controle sobre operadores assíncronos / esperados usando tipos de retorno assíncrono generalizado . A mesma técnica será usada dessa vez. Vamos começar com a classe "Reader", que será usada como resultado do tipo de operações assíncronas:
[AsyncMethodBuilder(typeof(ReaderTaskMethodBuilder<>))] public class Reader<T> : INotifyCompletion, IReader { ...
A turma tem duas responsabilidades diferentes (teoricamente, poderíamos criar duas turmas):
- Extrair algum valor de um contexto quando o contexto é aplicado
- Criação de uma lista vinculada de instâncias do Reader que serão usadas para distribuir um contexto por uma hierarquia de chamadas.
Para cada responsabilidade, usaremos um construtor separado:
private readonly Func<object, T> _extractor;
Quando uma instância da classe Reader é usada como argumento do operador wait , a instância receberá um link para um delegado de continuação, que deve ser chamado apenas quando um contexto de execução for resolvido e podemos extrair (do contexto) alguns dados que serão usado na continuação.

Para criar conexões entre os "leitores" pai e filho, vamos criar o método:
private IReader _child; internal void SetChild(IReader reader) { this._child = reader; if (this._ctx != null) { this._child.SetCtx(this._ctx); } }
que será chamado dentro do ReaderTaskMethodBuilder :
public class ReaderTaskMethodBuilder<T> { ... public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IReader reader) { this.Task.SetChild(reader); } awaiter.OnCompleted(stateMachine.MoveNext); } public Reader<T> Task { get; } }
Dentro do método SetChild , chamamos SetCtx, que propaga um contexto para uma hierarquia e chama um extrator, se definido:
public void SetCtx(object ctx) { this._ctx = ctx; if (this._ctx != null) { this._child?.SetCtx(this._ctx); if (this._extractor != null) { this.SetResult(this._extractor(this._ctx)); } } }
SetResult armazena um valor extraído de um contexto e chama uma continuação:
internal void SetResult(T result) { this._result = result; this.IsCompleted = true; this._continuation?.Invoke(); }
No caso em que uma instância do Reader não possui um "extrator" inicializado, então SetResult deve ser chamado pelo ReaderTaskMethodBuilder quando uma máquina de estado gerada chega ao seu estado final.
O método Apply chama apenas SetCtx
public Reader<T> Apply(object ctx) { this.SetCtx(ctx); return this; }
Você pode encontrar todo o código no github ( se ainda não estiver bloqueado )
Agora, quero mostrar um exemplo mais realista de como o Reader assíncrono pode ser usado:
Clique para expandir o exemplo public static class ReaderTest { public class Configuration { public readonly int DataBaseId; public readonly string GreetingTemplate; public readonly string NameFormat; public Configuration(int dataBaseId, string greetingTemplate, string nameFormat) { this.DataBaseId = dataBaseId; this.GreetingTemplate = greetingTemplate; this.NameFormat = nameFormat; } } public static async Task Main() { int[] ids = { 1, 2, 3 }; Configuration[] configurations = { new Configuration(100, "Congratulations, {0}! You won {1}$!", "{0} {1}"), new Configuration(100, "¡Felicidades, {0}! Ganaste {1} $", "{0}"), }; foreach (var configuration in configurations) { foreach (var userId in ids) {
O programa mostra cumprimentos para alguns usuários, mas não sabemos seus nomes com antecedência, pois temos apenas seus IDs; portanto, precisamos ler essas informações em um "banco de dados". Para conectar-se ao banco de dados, precisamos conhecer algum identificador de conexão e, para criar uma saudação, precisamos de seu modelo e ... todas as informações são passadas implicitamente através do leitor assíncrono.
Injeção de dependência com o "Leitor" assíncrono
Em comparação com a implementação clássica, o leitor assíncrono tem uma falha - não podemos especificar um tipo de contexto passado. Essa limitação vem do fato de o compilador C # esperar apenas um único parâmetro de tipo genérico em uma classe de construtor de método assíncrono (talvez seja corrigido no futuro).
Por outro lado, não acho que seja crítico, pois na vida real, provavelmente, algum contêiner de injeção de dependência será passado como um contexto:
public static class Reader { public static Reader<TService> GetService<TService>() => Reader<TService>.Read<IServiceProvider>(serviceProvider => (TService)serviceProvider .GetService(typeof(TService))); }
... private static async Reader<string> Greet(string userName) { var service = await Reader.GetService<IGreater>(); return service.GreetUser(userName); } ...
( aqui você pode encontrar um exemplo completo ... )
Diferente do assíncrono "Talvez" , que eu não recomendo usar em nenhum código de produção (por causa do problema com finalmente os blocos), consideraria o uso do leitor assíncrono em projetos reais como uma substituição (ou "além de") do abordagem tradicional de injeção de dependência (quando todas as dependências são passadas para um construtor de classe), pois o Reader tem algumas vantagens:
- Não há necessidade nas propriedades de classe que armazenam links para recursos injetados. Na verdade, não há necessidade de classes - toda a lógica pode ser implementada em métodos estáticos.
- O uso do Reader incentivará a criação de código sem bloqueio, pois todos os métodos serão assíncronos e nada impedirá que os desenvolvedores usem versões sem bloqueio de operações de entrada / saída.
- O código será um pouco mais "legível" - sempre que virmos o Reader como resultado de algum método, saberemos que ele requer acesso a algum contexto implícito.
- O leitor assíncrono não usa reflexão.
É claro que pode haver alguns argumentos contra o uso do Reader, mas, de qualquer forma, o principal objetivo desses artigos é mostrar como os padrões, que foram inicialmente projetados para linguagens funcionais, podem ser adotados para o estilo imperativo de codificação, que se acredita ser mais simples. entender pela maioria das pessoas.