Oragon.RabbitMQ está no forno!
Dando continuidade à nossa série, abordarei a ideia central pelo qual adoto Minimal API’s e por que elas tem tanto poder na modelagem.
É hora de abrir a caixa de pandora e falar sobre modelagem de API’s agnósticas.
Contexto
Quando comecei a desenvolver minhas primeiras Web API’s, lá por volta de 2004 ~ 2006, já estava habituado com ao menos 3 camadas claras e rígidas.
Sem bairrismos
Graças à minha experiência anterior com Delphi, ASP, JScript, VBScript, VB .NET e C#, minha taxa de bairrismo e ranço a respeito de outras linguagens e plataformas, sempre beirou zero.
Foi assim que olhei para o lado, mirei no Java, concorrente direto e mais velho.
Intuitivamente acreditei estar claro que passaríamos pelos mesmos dilemas do Java aqui no .NET. Essa intuição me ajudou a ir mais rápido e mais longe, antes da maioria. Simplesmente por entender que o que viveríamos aqui, outras comunidades já haviam vivenciado, em especial a comunidade Java já havia vivenciado.
Assim tive mais tempo para me adaptar. Bastava entender a direção e me acostumar com o que era disruptivo. Então ao invés de supor que tinha um papel em branco, tendo de escolher entre todos os assuntos possíveis para estudar, baseado somente no meu gosto e opinião, optei por pegar um caminho diferente e olhar o que já havia sido trilhado na comunidade java.
Pois aqui não seria muito diferente.
E deu certo!
Dessa forma nunca perdi muito tempo torcendo o nariz, .NET era minha plataforma favorita, mas sempre olhei o Java como um primo mais velho que sempre teria algo a nos ensinar.
Eu sabia que era necessário ter certo discernimento para entender o que era erro de design e o que não era. Mas eu começando a desenvolver esse olhar. Mas, certamente, eles como comunidade, plataforma e arquitetura, estavam anos na nossa frente.
O incômodo
Por volta dessa época, algo que sempre me incomodou nas documentações e material técnico sobre Web API’s é o acoplamento.
A gente aprendia desde cedo que acoplamento é sempre negativo!
Um campo de estudo que já me cativava naquela época eram as aplicações distribuídas, e a compreensão de que poderíamos distribuir aplicações e não necessariamente usar HTTP como endpoint se tornava natural.
Naquela época tínhamos Remoting, MSMQ, Enterprise Services. Pela proximidade com o Windows já tínhamos componentes COM+ no passado, então compreender esses desenhos era algo importante para mim naquela época.
Como repito, sempre me pergunto:
- O que os profissionais fazem?
- Como eles pensam?
- Por que pensam dessa forma?
- Por que escolhem essa abordagem e tecnologia?
- O que há aqui que não estou vendo?
Esse ceticismo me permitiu buscar respostas ao invés de criar pré-concepções burras e rasas.
Assim, com o passar do tempo, ver um DataSet do ADO.NET, uma Session do NHibernate, ou um dbml do Linq To SQL, ou mesmo um DbContext do Entity Framework em uma página ASPX (Webforms) sempre me gerou imenso desconforto.
Mais tarde ver esse mesmo tipo de exemplo com controllers com DbContext do EF continua me gerando ojeriza.
Spring .NET me ensinou coisas incríveis…
Como citei em Sobre Ports and Adapters, Agnostic Services e modelagem de Serviços, meu contato com a versão .NET do Spring me permitiu compreender injeção de dependência, de uma forma pura. Muito diferente do mecanismo adotado no ASP.NET com as ServiceCollections.
Naquela época, 2006-2008, aqui na comunidade .NET estava nascendo um projeto chamado Castle.Windsor, um container de injeção de dependência baseado em Register/Resolver, um padrão com qual já briguei muito no passado.
Como se não bastassem as limitações do pattern e a discussão sobre ser ou não anti-pattern, há mais de 10 anos sinalizamos quão nocivo é a abordagem de register/resolver para a modelagem.
Referências
IoC e Dependency Injection – Os erros comuns de 2014
Register/Resolver e suas implicações para a modelagem e reaproveitamento de 2020
Abandonei as discussões sobre Register/Resolver quando a Microsoft decidiu adotar essa mesma abordagem no .NET Core.
Uma pena, todos perdem!
Com o Spring .NET, pude ver como realizar implementações de Serviços no Backend que pudessem ser expostos por diversas tecnologias, usando o máximo de performance na comunicação binária com Remoting, a versatilidade do Json, ou a compatibilidade do XML/WebService, a partir de somente um código, era incrível.
Estou falando de uma época pré-WCF, que de forma prática, tornou burocrática a mesma abordagem.
Com Spring.NET, só precisávamos construir um assembly com interfaces e dto’s, e disponibilizar para nossos clientes.
A exposição do host e o consumo no client eram abstraídos por simples configurações.
A chegada do WCF
O WCF trouxe uma necessidade de atributos de marcação nessas mesmas interfaces e DTO’s.
Isso fez com que sujássemos nossas entidades no WCF, diferente do Spring.NET, onde não havia essa demanda. Além disso a necessidade de configuração específica no WCF era muito maior, e juntar os 2 gerava um esforço cognitivo alto.
A abordagem do Spring.NET, onde você escreveria apenas uma vez e hospedaria e consumiria em diversos protocolos é herança do conceito de Java bean.
O Spring cuidava de criar proxies para nossas classes, dinamicamente. Tanto par hospedar quanto para consumir.
Então uma instância de nossa classe era envelopada por um proxy que lidava com a complexidade de comunicação remota, tornando o desenvolvimento local ou remoto, IDENTICO!
Hoje você pode ver algo parecido com o Refit, entretanto Refit se prende ao HTTP, enquanto a ideia do Spring.Services (pacote que lida com essas abstrações) é possibilitar usarmos múltiplos protocolos simultaneamente.
Só que naquela época, nenhum detalhe via atributo era necessário.
Agnostic Service
O pattern que permite criarmos serviços que não dependem de nenhuma tecnologia de exposição de serviços (wcf, webservice, webhttp, webapi, grpc, amqp etc), e podem ser expostos em diversos protocolos e tecnologias, antigos ou posteriores à sua criação, é o agnostic service.
Um serviço agnóstico não sabe qual tecnologia o hospeda. Detalhes específicos das tecnologias de hospedagem são abstraídos, e os serviços não tomam conhecimento da estratégia de deployment, e são usados remotamente como se locais estivessem.
A ideia é que você tenha classes simples que não misturem conceitos nem dependências específicas do modo de exposição.
Então a implementação abaixo, por exemplo, poderia se exposta por uma dúzia de protocolos.
public interface IEmailService { Task SendEmailAsync(EmailCommand command); } public class EmailService: IEmailService { public async Task SendEmailAsync(EmailCommand command) { ... send email using smtp ... } }
A mágica é que a infraestrutura de injeção de dependência é quem precisa saber como criar o proxy, ou a instância diretamente.
Mas do ponto de vista de uso, é natural, e se comporta como se fosse uma dependência local. Mas a operação pode estar sendo executada de fato no servidor ao lado, em outro datacenter, ou do outro lado do mundo.
Nesse cenário, de email, onde o feedback não precisa acontecer imediatamente, o serviço de email pode até estar offline, caso adote uma estratégia de comunicação via mensageria.
O poder dessa abordagem é que você tem um código abaixo:
public class OtherService (IEmailService emailService) { public async Task DoSomething(EmailCommand command) { ... emailService.SendEmailAsync(new EmailCommand(){ ... }); ... } }
Mas quais são as vantagens?
De um lado, você reduz o acoplamento entre o serviço de sua tecnologia de hospedagem.
Também temos o aumento da capacidade de reaproveitar de código, sem necessidade de copy-and-paste ou reescrita.
A decisão sobre a topologia está fora do código, fora da implementação e isso reduz o esforço cognitivo.
Essa abordagem delega a decisão sobre topologia para um momento posterior ao desenvolvimento, sem afetar o desenvolvimento.
Também tira do radar decisões sobre topologia, permitindo que o desenvolvimento e construção da funcionalidade não dependa de uma decisão dessas.
No mundo “real”, essa decisão gera um compromisso de longo prazo, algo que se mudar, gera esforço de codificação, gera custo e relativo trabalho.
Com essa abordagem, não. É simples como quem troca de roupa.
Essa decisão pode ser tomada mais tarde e pode ser alterada com pouco esforço e zero mudança no código.
A abordagem permite, o que na minha opinião é o melhor benefício: não precisar tomar a decisão sobre a estratégia de distribuição até que fosse realmente necessário distribuir, mas também permitindo reagir rápido se necessário, sem necessidade de reconstrução.
Boa parte, (ou melhor, todas) das estimativas prévias sobre consumo que que recebi na minha carreira, se mostraram equivocadas e desconectados da realidade.
De um lado estimativas descabidas, abaixo da realidade, de outro muito acima do esperado. E todas ignoravam eventos casuais que produziam aumento significativo de tráfego.
Então ter tranquilidade e uma arquitetura que não me deixasse “vendido” e me permita em minutos ou no máximo em horas, reorganizar essas coisas para ter uma topologia diferente, isso sim significa PODER.
A maioria das abordagens tradicionais demandaria dias ou até semanas.
Minimal API’s são os proxies dinâmicos do passado
Com o Spring.NET, o pacote Spring.Services cuidava de criar um proxy para hospedar nossos serviços.
Agora, quase 2 décadas depois, as Minimal API’s podem ser um bom substituto para o antigo proxy AOP ou DynamicProxy, usado nesses casos.
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/", (EmailCommand command, [FromServices]IEmailService emailService) => emailService.SendEmailAsync(command));“ record EmailCommand(string ToAddress, string Text);
A abordagem adotada no Oragon.RabbitMQ é focada em desburocratizar o consumo de mensagens, sem perder a essência do RabbitMQ.
Enquanto a maioria das abordagens ignora ou permite ignorar, questões importantes do message broker, essa abordagem entrega uma versão projetada e otimizada pare os recursos do RabbitMQ.
O resultado é que a abordagem abaixo permite reaproveitar esse serviço de envio de email via HTTP e AMQP.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<IAMQPSerializer, SystemTextJsonAMQPSerializer>(); builder.Services.MapQueue<IEmailService, EmailCommand>(config => config .WithDispatchInRootScope() .WithAdapter((svc, msg) => svc.SendEmailAsync(msg)) .WithQueueName("mail“") .WithPrefetchCount(1) ); var app = builder.Build(); app.MapPost("/", (EmailCommand command, [FromServices]IEmailService emailService) => emailService.SendEmailAsync(command)); record EmailCommand(string ToAddress, string Text);
O mesmo código: multiplos endpoints.
Exposto de diversas formas, com muita simplicidade e elegância: ao mesmo tempo.
Estamos falando da criação de serviços que não fazem ideia de que estão inseridos em um contexto web ou amqp (RabbitMQ). Dessa forma eles são mais fáceis de se compreender, de manter, de evoluir e custam mais barato.
Essa abordagem não se esconde em uma implementação em um proxy, em vez disso criamos simples funções de adaptação, como nas linhas 7 e 16.
Se você acredita no MapGet/MapPost para receber um EmailCommand quando um request HTTP for enviado, não seria nada diferente entender que se a fila “mail” receber uma mensagem, você receberá um EmailCommand no MapQueue!
O princípio é o mesmo!
Claro que se você leu o texto, encontrará diferença entre o que eu fazia lá em 2006 e o que estamos entregando agora.
Naquela época aceitávamos grandes arquivos de configuração, o web.config já era um desses arquivos gigantes, hoje os tempos são outros, e a abordagem queridinha mudou.
Mas o conceito, continua e a essência do poder dos Serviços Agnósticos continua a mesma!
0 comentários