O CQRS / MediatR vale a pena ao desenvolver um aplicativo ASP.NET?

16

Ultimamente tenho pesquisado o CQRS / MediatR. Mas quanto mais eu detalhar, menos eu gosto. Talvez eu tenha entendido mal alguma coisa / tudo.

Então, ele começa incrível, alegando reduzir seu controlador a esse

public async Task<ActionResult> Edit(Edit.Query query)
{
    var model = await _mediator.SendAsync(query);

    return View(model);
}

O que se encaixa perfeitamente com as diretrizes finas do controlador. No entanto, deixa de fora alguns detalhes muito importantes - tratamento de erros.

Vamos analisar a Loginação padrão de um novo projeto MVC

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation(1, "User logged in.");
            return RedirectToLocal(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning(2, "User account locked out.");
            return View("Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Convertendo isso nos apresenta um monte de problemas do mundo real. Lembre-se que o objetivo é reduzi-lo a

public async Task<IActionResult> Login(Login.Command command, string returnUrl = null)
{
    var model = await _mediator.SendAsync(command);

    return View(model);
}

Uma solução possível para isso é retornar um em CommandResult<T>vez de um modele depois manipular o CommandResultfiltro em uma ação pós. Como discutido aqui .

Uma implementação do CommandResultpoderia ser assim

public interface ICommandResult  
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
}

fonte

No entanto, isso realmente não resolve o nosso problema na Loginação, porque existem vários estados de falha. Poderíamos adicionar esses estados extras de falha, ICommandResultmas isso é um ótimo começo para uma classe / interface muito inchada. Pode-se dizer que não está de acordo com a responsabilidade única (SRP).

Outro problema é o returnUrl. Nós temos esse return RedirectToLocal(returnUrl);pedaço de código. De alguma forma, precisamos lidar com argumentos condicionais com base no estado de sucesso do comando. Embora eu ache que isso possa ser feito (não tenho certeza se o ModelBinder pode mapear os argumentos FromBody e FromQuery ( returnUrlé FromQuery) para um único modelo). Só podemos imaginar que tipo de cenários malucos poderiam surgir no caminho.

A validação do modelo também se tornou mais complexa junto com o retorno de mensagens de erro. Tome isso como um exemplo

else
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return View(model);
}

Anexamos uma mensagem de erro junto com o modelo. Esse tipo de coisa não pode ser feito usando uma Exceptionestratégia (como sugerido aqui ) porque precisamos do modelo. Talvez você possa obter o modelo do Requestmas seria um processo muito envolvido.

Então, apesar de tudo, estou tendo dificuldade para converter essa ação "simples".

Eu estou procurando por entradas. Estou totalmente errado aqui?

Snæbjørn
fonte
6
Parece que você já entende muito bem as preocupações relevantes. Existem muitas "balas de prata" por aí que têm exemplos de brinquedos que provam sua utilidade, mas que inevitavelmente caem quando são pressionados pela realidade de uma aplicação real e real.
Robert Harvey
Confira Comportamentos do MediatR. É basicamente um pipeline que permite lidar com preocupações transversais.
fml

Respostas:

14

Eu acho que você está esperando muito do padrão que está usando. O CQRS foi projetado especificamente para resolver a diferença de modelo entre consulta e comandos no banco de dados , e o MediatR é apenas uma biblioteca de mensagens em processo. O CQRS não pretende eliminar a necessidade de lógica de negócios como você espera. O CQRS é um padrão para acesso a dados, mas seus problemas estão na camada de apresentação - redirecionamentos, visualizações, controladores.

Eu acho que você pode estar aplicando incorretamente o padrão CQRS à autenticação. Com o login, ele não pode ser modelado como um comando no CQRS porque

Comandos: altere o estado de um sistema, mas não retorne um valor
- Martin Fowler CommandQuerySeparation

Na minha opinião, a autenticação é um domínio ruim para o CQRS. Com a autenticação, você precisa de um fluxo de solicitação e resposta síncrono e altamente consistente, para que você possa 1. verificar as credenciais do usuário 2. criar uma sessão para o usuário 3. lidar com qualquer uma das várias situações de ponta que você identificou 4. conceder ou negar imediatamente o usuário em resposta.

O CQRS / MediatR vale a pena ao desenvolver um aplicativo ASP.NET?

CQRS é um padrão que possui usos muito específicos. Seu objetivo é modelar consultas e comandos em vez de ter um modelo para registros, conforme usado no CRUD. À medida que os sistemas se tornam mais complexos, as demandas de visualizações geralmente são mais complexas do que apenas mostrar um único registro ou um punhado de registros, e uma consulta pode modelar melhor as necessidades do aplicativo. Da mesma forma, os comandos podem representar alterações em muitos registros, em vez de CRUD, que você altera registros únicos. Martin Fowler adverte

Como qualquer padrão, o CQRS é útil em alguns lugares, mas não em outros. Muitos sistemas se encaixam em um modelo mental CRUD, e assim devem ser feitos nesse estilo. O CQRS é um salto mental significativo para todos os envolvidos, portanto não deve ser enfrentado, a menos que o benefício valha a pena. Embora tenha me deparado com usos bem-sucedidos do CQRS, até agora a maioria dos casos em que me deparei não foi tão boa, com o CQRS visto como uma força significativa para colocar um sistema de software em sérias dificuldades.
- Martin Fowler CQRS

Portanto, para responder à sua pergunta, o CQRS não deve ser o primeiro recurso ao projetar um aplicativo quando o CRUD for adequado. Nada na sua pergunta me deu a indicação de que você tem um motivo para usar o CQRS.

Quanto ao MediatR, é uma biblioteca de mensagens em processo, que visa dissociar solicitações do tratamento de solicitações. Você deve decidir novamente se ele melhorará seu design para usar esta biblioteca. Pessoalmente, não sou um defensor das mensagens em processo. O acoplamento flexível pode ser alcançado de maneiras mais simples que as mensagens, e eu recomendo que você comece por aí.

Samuel
fonte
1
Eu concordo 100%. O CQRS é um pouco exagerado, então eu pensei que "eles" viram algo que eu não vi. Porque estou tendo dificuldades para ver os benefícios do CQRS em aplicativos da web CRUD. Até agora, o único cenário é CQRS + ES que faz sentido para mim.
Snæbjørn
Um cara do meu novo trabalho decidiu colocar o MediatR no novo sistema ASP.Net, reivindicando-o como uma arquitetura. A implementação que ele fez não é DDD, nem SOLID, nem DRY, nem KISS. É um pequeno sistema cheio de YAGNI. E começou muito depois de alguns comentários como o seu, incluindo o seu. Estou tentando descobrir como refato o código para adaptar sua arquitetura gradualmente. Eu tinha a mesma opinião sobre o CQRS fora de uma camada de negócios e fico feliz que vários desenvolvedores experientes pensem dessa maneira.
MFedatto 24/09
É um pouco irônico afirmar que a idéia de incorporar o CQRS / MediatR pode estar associada a muitos YAGNI e à falta de KISS, quando na verdade algumas das alternativas populares, como o padrão Repository, promovem o YAGNI inchando a classe do repositório e forçando interfaces para especificar muitas operações CRUD em todos os agregados raiz que desejam implementar essas interfaces, geralmente deixando esses métodos sem uso ou preenchidos com exceções "não implementadas". Como o CQRS não usa essas generalizações, ele pode implementar apenas o necessário.
Lesair Valmont 6/08/19
O Repositório @LesairValmont deve ser apenas CRUD. "especificar muitas operações CRUD" deve ser apenas 4 (ou 5 com "lista"). Se você tiver padrões de acesso à consulta mais específicos, eles não deverão estar na interface do repositório. Eu nunca tive um problema com métodos de repositório não utilizados. Você pode dar um exemplo?
Samuel
@ Samuel: Eu acho que o padrão do repositório é perfeitamente adequado para certos cenários, assim como o CQRS. Na verdade, em um aplicativo grande, haverá algumas partes cujo melhor ajuste será o padrão do repositório e outras que seriam mais beneficiadas pelo CQRS. Depende de muitos fatores diferentes, como a filosofia seguida nessa parte do aplicativo (por exemplo, baseada em tarefas (CQRS) vs. CRUD (repo)), o ORM sendo usado (se houver), a modelagem do domínio ( por exemplo, DDD). Para catálogos CRUD simples, o CQRS é definitivamente um exagero, e alguns recursos de colaboração em tempo real (como um bate-papo) também não usariam.
Lesair Valmont 9/09/19
10

O CQRS é mais uma coisa de gerenciamento de dados e não tende a sangrar muito em uma camada de aplicativo (ou Domínio, se preferir, pois costuma ser usado com mais freqüência em sistemas DDD). Seu aplicativo MVC, por outro lado, é um aplicativo de camada de apresentação e deve estar razoavelmente bem separado do núcleo de consulta / persistência do CQRS.

Outra coisa que vale a pena notar (dada a comparação do Loginmétodo padrão e o desejo de controladores thin): eu não seguiria exatamente o código padrão de modelos / clichê do ASP.NET como algo que nos preocuparia com as práticas recomendadas.

Também gosto de controladores finos, porque são muito fáceis de ler. Cada controlador que eu tenho geralmente tem um objeto de "serviço" que ele emparelha com o que lida essencialmente com a lógica exigida pelo controlador:

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) {

    var result = _service.Login(model);
    switch (result) {
        case result.lockout: return View("Lockout");
        case result.ok: return RedirectToLocal(returnUrl);
        default: return View("GeneralError");
    }
}

Ainda é fino o suficiente, mas não mudamos realmente o funcionamento do código, apenas delegamos a manipulação ao método de serviço, que realmente não serve para nada além de facilitar a digestão das ações do controlador.

Lembre-se de que essa classe de serviço ainda é responsável por delegar a lógica ao modelo / aplicativo, conforme necessário; na verdade, é apenas uma pequena extensão do controlador para manter o código limpo. Os métodos de serviço também são geralmente bastante curtos.

Não sei se o mediador faria algo conceitualmente diferente: mover alguma lógica básica do controlador para fora do controlador e para outro lugar a ser processado.

(Eu nunca tinha ouvido falar desse MediatR antes, e uma rápida olhada na página do github não parece indicar que é algo inovador - certamente não algo como o CQRS - na verdade, parece algo como apenas mais uma camada de abstração. pode colocar para complicar o código, tornando-o mais simples, mas essa é apenas a minha opinião inicial)

jleach
fonte
5

Eu recomendo que você veja a apresentação da NDC de Jimmy Bogard sobre sua abordagem para modelar solicitações http https://www.youtube.com/watch?v=SUiWfhAhgQw

Você terá uma idéia clara do uso do Mediatr.

Jimmy não tem uma adesão cega a padrões e abstrações. Ele é muito pragmático. O Mediatr limpa as ações do controlador. Quanto ao tratamento de exceções, eu envio isso para uma classe pai chamada algo como Execute. Então você acaba com uma ação de controlador muito limpa.

Algo como:

public bool Execute<T>(Func<T> messageFunction)
{
    try
    {
        messageFunction();

        return true;
    }
    catch (ValidationException exception)
    {
        Errors = string.Join(Environment.NewLine, exception.Errors.Select(e => e.ErrorMessage));
        Logger.LogException(exception, "ValidationException caught in SiteController");
    }
    catch (SiteException exception)
    {
        Errors = exception.Message;
        Logger.LogException(exception);
    }
    catch (DbEntityValidationException dbEntityValidationException)
    {
        // Retrieve the error messages as a list of strings.
        var errorMessages = dbEntityValidationException.EntityValidationErrors
                .SelectMany(x => x.ValidationErrors)
                .Select(x => x.ErrorMessage);

        // Join the list to a single string.
        var fullErrorMessage = string.Join("; ", errorMessages);

        // Combine the original exception message with the new one.
        var exceptionMessage = string.Concat(dbEntityValidationException.Message, " The validation errors are: ", fullErrorMessage);

        Logger.LogError(exceptionMessage);

        // Throw a new DbEntityValidationException with the improved exception message.
        throw new DbEntityValidationException(exceptionMessage, dbEntityValidationException.EntityValidationErrors);                
    }
    catch (Exception exception)
    {
        Errors = "An error has occurred.";
        Logger.LogException(exception, "Exception caught in SiteController.");
    }

    // used to indicate that any transaction which may be in progress needs to be rolled back for this request.
    HttpContext.Items[UiConstants.Error] = true;

    Response.StatusCode = (int)HttpStatusCode.InternalServerError; // fail

    return false;
}

O uso se parece um pouco com isso:

[Route("api/licence")]
public IHttpActionResult Post(LicenceEditModel licenceEditModel)
{
    var updateLicenceCommand = new UpdateLicenceCommand { LicenceEditModel = licenceEditModel };
    int licenceId = -1;

    if (Execute(() => _mediator.Send(updateLicenceCommand)))
    {
        return JsonSuccess(licenceEditModel);
    }

    return JsonError(Errors);
}

Espero que ajude.

DavidRogersDev
fonte
4

Muitas pessoas (eu também fiz) confundem padrão com uma biblioteca. CQRS é um padrão, mas o MediatR é uma biblioteca que você pode usar para implementar esse padrão

Você pode usar o CQRS sem o MediatR ou qualquer biblioteca de mensagens em processo e o MediatR sem o CQRS:

public interface IProductsWriteService
{
    void CreateProduct(CreateProductCommand createProductCommand);
}

public interface IProductsReadService
{
    ProductDto QueryProduct(Guid guid);
}

O CQS ficaria assim:

public interface IProductsService
{
    void CreateProduct(CreateProductCommand createProductCommand);
    ProductDto QueryProduct(Guid guid);
}

Na verdade, você não precisa nomear seus modelos de entrada como "Comandos", como acima CreateProductCommand. E entrada de suas consultas "Consultas". Comando e consultas são métodos, não modelos.

O CQRS trata da segregação de responsabilidades (os métodos de leitura devem estar em um local separado dos métodos de gravação - isolados). É uma extensão do CQS, mas a diferença está no CQS. Você pode colocar esses métodos em 1 classe. (sem segregação de responsabilidades, apenas separação de comando e consulta). Veja separação vs segregação

Em https://martinfowler.com/bliki/CQRS.html :

Na essência, está a noção de que você pode usar um modelo diferente para atualizar informações do que o modelo usado para ler informações.

Há confusão no que diz, não se trata de ter um modelo separado para entrada e saída, é sobre separação de responsabilidades.

CQRS e limitação de geração de ID

Há uma limitação que você enfrentará ao usar o CQRS ou CQS

Tecnicamente, na descrição original, os comandos não devem retornar nenhum valor (vazio) que eu acho estúpido, porque não há uma maneira fácil de obter a ID gerada de um objeto recém-criado: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

então você precisa gerar um ID cada vez, em vez de permitir que o banco de dados faça isso.


Se você quiser saber mais: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
fonte
1
Desafio a sua afirmação de que um comando do CQRS para persistir novos dados em um banco de dados, incapaz de retornar um ID gerado recentemente pelo banco de dados, é "estúpido". Prefiro pensar que isso é uma questão filosófica. Lembre-se de que grande parte do DDD e do CQRS trata da imutabilidade dos dados. Quando você pensa duas vezes, começa a perceber que o mero ato de persistir dados é uma operação de mutação de dados. E não se trata apenas de novos IDs, mas também de campos preenchidos com dados padrão, acionadores e procs armazenados que também podem alterar seus dados.
Lesair Valmont 6/08/19
Claro que você pode enviar algum tipo de evento como "ItemCreated" com um novo item como argumento. Se você está lidando apenas com o protocolo de solicitação-resposta e usando o CQRS "true", o ID deve ser conhecido antecipadamente, para que você possa passá-lo para uma função de consulta separada - absolutamente nada de errado nisso. Em muitos casos, o CQRS é apenas um exagero. Você pode viver sem ele. É apenas uma maneira de estruturar seu código e isso depende principalmente de quais protocolos você usa também.
Konrad
E você pode conseguir imutabilidade de dados sem CQRS
Konrad