DEV Community

Rodolpho Alves
Rodolpho Alves

Posted on • Originally published at Medium on

Cell CMS — Design Patterns e Endpoints

Cell CMS — Design Patterns e Endpoints

Photo by Jason Leung on Unsplash

No último post falamos sobre persistência de dados, configuramos os models utilizando o EntityFrameworkCore e ficamos a um último passo de ter alguma coisa rodando! No post de hoje vamos falar brevemente sobre como estruturar nossos endpoints , mediator design pattern e CQRS (Command/Query Responsability Segregation).

Começaremos um novo branch feature/endpoints para o que for implementado durante este post. Este branch já contem alguns ajustes de features feitas fora de posts: feature/refactor-context e feature/limpeza-codigo. Vamos ao conteúdo!

Uma breve história sobre Patterns

Um assunto bem clássico do universo de TI são os design patterns. Um assunto vasto e que não seria possível abordar em um simples post, talvez em uma série de posts com vastos exemplos para não tornar a leitura entediante!

Para quem já trabalhou com tecnologias web o termo MVC soará familiar. Essa sigla significa Model-View-Controller e é um dos design patterns mais comuns! Em antigas versões, ainda no .NET Framework, o próprio “apelido” do framework era ASPNET MVC. Basicamente o MVC divide a separação em três camadas lógicas:

  1. Model : Camada de persistência dos dados, onde a informação será armazenada
  2. Controller : Camada onde será feito o processamento dos comandos, sejam eles oriundos das View ou Models, e encaminhamento dos resultados
  3. View : Camada de apresentação dos dados, onde a informação será exibida ao usuário

Recomendo a leitura do próprio artigo na Wikipediapara quem quiser mais referencial teórico.

Este pattern funciona muito bem, porém ele foi sendo complementado e evoluído com o tempo.

Por exemplo, no universo .NET, é muito comum ver projetos com ViewModels , uma camada adicional para ajustar os dados para a exibição ao usuário. A biblioteca AutoMapper é, até hoje, uma das mais baixadas por causa de DTOs e ViewModels!

Outro recurso interessante (e bem visual) é o Refactoring Guru, onde são apresentados diversos tipos de Patterns com explicação gráfica, snippets e tudo! Se você for mais da literatura clássica a recomendação é o Gang of Four (GOF) Design Patterns!

CQRS

Nossa API, por definição, não possuirá Views. Temos duas camadas principais:

  1. Controllers: Receberá as inputs e emitará outputs através de JSON e HTTP
  2. Models: Persistência dos dados , através do EntityFrameworkCore

Cada C ontroller irá conter, no mínimo, as operações CRUD (Create, Read, Update e Delete) para um model. Cada operação será , necessáriamente, um endpoint.

Podemos dizer então que cada controller realizará Commands ou Queries sobre as entidades que eles são responsáveis. Commands são operações que alteram o estado da aplicação , como criações, atualizações e remoções. Queries são operações que não alteram o estado da aplicação , não importa o quão completas sejam. Os Commands e as Queries serão processadas por Handlers específicos, podendo passar por Pipelines pré ou pós execução.

Vamos ver como isso ficaria, abaixo estão dois diagramas. Um pensando no MVC e outro pensando no CQRS.

Endpoints, no padrão MVC

Endpoints, ainda com Models e Controllers mas utilizando Commands. Queries e Handlers.

Como toda abordagem, existem suas vantagens e desvantagens para cada um dos padrões. Eu, pessoalmente, sigo a seguinte lógica:

  • MVC: Projetos pequenos , que provavelmente não serão mantidos, e protótipos.
  • CQRS : Projetos que podem expandir ou com complexidade mais alta de regras de negócio.

“Mas Rodolpho, o Cell CMS será um projeto com complexidade alta?”

Não! Mas é um projeto que pretendo seguir dando manutenção e gostaria que o código fosse o mais limpo possível para que qualquer pessoa possa olhar o código e entender o que está acontecendo.

Criando nossos endpoints

Antes de começarmos com o CQRS vamos instalar uma biblioteca que nos auxiliará a manter o Mediator Pattern (que se encaixa perfeitamente com o CQRS). Esta biblioteca é a MediatR e a MediatR.Extensions.Microsoft.DependencyInjection . Portanto abra a solution, clique direito na API, Manage NuGet Packages e instale-os!

Antes de configurarmos, vamos ver as duas Interfaces mais importantes do MediatR:

  • IRequest : Identifica que a classe é uma requisição. O tipo T é o possível retorno, caso tenha, da requisição. Nossos Commands e Queries implementarão esta interface.
  • IRequestHandler, T?> : Identifica que a classe é um Handler para uma Requisição que, caso tenha retorno, retorna um objeto do tipo T. Nossos Handlers implementarão esta interface.

Com isso fora do caminho vamos organizar nossas operações em Commands, Requests e Model afetado:

  • Feed : criar novo, atualizar existente, ler todos, deletar
  • Tag : criar nova, atualizar existente, ler por feed, ler todas, deletar
  • Content : criar novo, atualizar existente, ler por feed, ler por tag, deletar

Para manter o post curto utilizarei como exemplo as operações do Feed! Porém, no GitHub, estão disponíveis as implementações para todos os outros.

Para começar vamos criar duas novas pastas na Api: Features e uma subpasta Feed. Dentro da pasta feed vamos criar 5 arquivos, um para cada command/query:

  1. CreateFeed : Conterá o command e a lógica para criar um novo feed
  2. ListAllFeeds : Conterá a query para listar todos os feeds
  3. UpdateFeed : Conterá o command e a lógica para atualizar um feed existente
  4. DeleteFeed: Conterá o command e a lógica para deletar um feed existente

O código das 5 classes está disponível abaixo. Para manter a API com poucos arquivos e facilitar a navegação estou colocando o Handler de cada IRequest no mesmo arquivo. Isso manterá o código mais fácil de navegar, ao custo de repetir alguns trechos de código (especialmente no construtor e, futuramente, com o Polly)

using System;
using System.Threading;
using System.Threading.Tasks;
using CellCms.Api.Models;
using MediatR;
namespace CellCms.Api.Features.Feeds
{
/// <summary>
/// Command para criar um novo Feed.
/// </summary>
public class CreateFeed : IRequest<Feed>
{
/// <summary>
/// Nome do novo feed.
/// </summary>
public string Nome { get; set; }
}
/// <summary>
/// Handler para a criação de feeds.
/// </summary>
public class CreateFeedHandler : IRequestHandler<CreateFeed, Feed>
{
private readonly CellContext _context;
public CreateFeedHandler(CellContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public Task<Feed> Handle(CreateFeed request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
// TODO: Depois refatorar para que possamos utilizar uma validação melhor!
if (string.IsNullOrWhiteSpace(request.Nome))
{
throw new ArgumentException("Um nome deve ser informado", nameof(request.Nome));
}
// TODO: Futuramente mapear de maneira automatica
var feed = new Feed
{
Nome = request.Nome
};
// Separamos os métodos para que o compilador possa otimizar.
// No método principal realizamos apenas operações sincronas
// No método interno realizamos as operações assíncronas
return CreateFeedInternalAsync(feed, cancellationToken);
}
private async Task<Feed> CreateFeedInternalAsync(Feed feed, CancellationToken cancellationToken)
{
_context.Feeds.Add(feed);
await _context.SaveChangesAsync(cancellationToken);
return feed;
}
}
}
view raw CreateFeed.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CellCms.Api.Features.Feeds
{
/// <summary>
/// Command para deletar um feed.
/// </summary>
public class DeleteFeed : IRequest
{
[FromRoute(Name = "id")]
public int Id { get; set; }
}
/// <summary>
/// Handler para commands de delete.
/// </summary>
public class DeleteFeedHandler : IRequestHandler<DeleteFeed>
{
private readonly CellContext _context;
public DeleteFeedHandler(CellContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public Task<Unit> Handle(DeleteFeed request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return DeleteFeedInternalAsync(request.Id, cancellationToken);
}
private async Task<Unit> DeleteFeedInternalAsync(int id, CancellationToken cancellationToken)
{
var existingFeed = await _context
.Feeds
.FindAsync(new object[] { id }, cancellationToken: cancellationToken);
if (existingFeed is null)
{
throw new KeyNotFoundException($"Não foi encontrado um feed com id {id}");
}
_context.Feeds.Remove(existingFeed);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
view raw DeleteFeed.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CellCms.Api.Features.Feeds
{
/// <summary>
/// Query para listar
/// </summary>
public class ListAllFeeds : IRequest<IEnumerable<ListFeed>> { }
/// <summary>
/// Feed para exibição em lista.
/// </summary>
public class ListFeed
{
public int Id { get; set; }
public string Nome { get; set; }
/// <summary>
/// Lista dos nomes das tags.
/// </summary>
public IEnumerable<string> TagsNome { get; set; }
/// <summary>
/// Lista dos títulos dos conteúdos.
/// </summary>
public IEnumerable<string> ContentsTitulo { get; set; }
}
/// <summary>
/// Handler para listar feeds.
/// </summary>
public class ListAllFeedsHandler : IRequestHandler<ListAllFeeds, IEnumerable<ListFeed>>
{
private readonly CellContext _context;
public ListAllFeedsHandler(CellContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public Task<IEnumerable<ListFeed>> Handle(ListAllFeeds request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return ListAllFeedsInternalAsync(cancellationToken);
}
private async Task<IEnumerable<ListFeed>> ListAllFeedsInternalAsync(CancellationToken cancellationToken)
{
var result = await _context
.Feeds
.AsNoTracking() // Indicando para o EF Core que vamos fazer apenas operações de leitura
.Include(f => f.Contents)
.Include(f => f.Tags)
.ToListAsync(cancellationToken);
// Futuramente vamos fazer o mapeamento automaticamente!
return result.Select(
r => new ListFeed
{
Id = r.Id,
Nome = r.Nome,
ContentsTitulo = r.Contents.Select(c => c.Titulo),
TagsNome = r.Tags.Select(t => t.Nome)
}
);
}
}
}
view raw ListAllFeeds.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace CellCms.Api.Features.Feeds
{
/// <summary>
/// Command para editar um feed.
/// </summary>
public class UpdateFeed : IRequest
{
public int Id { get; set; }
public string Nome { get; set; }
}
/// <summary>
/// Handler para updates.
/// </summary>
public class UpdateFeedHandler : IRequestHandler<UpdateFeed>
{
private readonly CellContext _context;
public UpdateFeedHandler(CellContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public Task<Unit> Handle(UpdateFeed request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return UpdateFeedInternalAsync(request.Id, request.Nome, cancellationToken);
}
private async Task<Unit> UpdateFeedInternalAsync(int id, string nome, CancellationToken cancellationToken)
{
var existingFeed = await _context
.Feeds
.FindAsync(new object[] { id }, cancellationToken);
if (existingFeed is null)
{
throw new KeyNotFoundException($"Não foi encontrado um feed com id {id}");
}
existingFeed.Nome = nome;
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
view raw UpdateFeed.cs hosted with ❤ by GitHub

Agora vamos criar nosso FeedsController , tendo um método para cada operação também:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace CellCms.Api.Features.Feeds
{
[Route("api/[controller]")]
public class FeedController : ControllerBase
{
private readonly ILogger<FeedController> _logger;
private readonly IMediator _mediator;
public FeedController(ILogger<FeedController> logger, IMediator mediator)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
[HttpPost]
[Authorize]
public async Task<IActionResult> Create([FromBody] CreateFeed command)
{
// TODO: Futuramente vamos implementar estas validações
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
var result = await _mediator.Send(command);
return Created(string.Empty, result);
}
catch (ArgumentException ex)
{
// Futuramente será tratado pelo ModelState.
return BadRequest(ex.Message);
}
catch (Exception ex)
{
// Caso seja algum erro que não esperavamos, vamos fazer um log decente deste erro.
_logger.LogError(ex, "Erro ao tentar criar um novo Feed: {@Command}", command);
throw;
}
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var query = new ListAllFeeds();
try
{
var result = await _mediator.Send(query);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao tentar listar todos os feeds");
throw;
}
}
[HttpPut]
[Authorize]
public async Task<IActionResult> Update([FromBody] UpdateFeed command)
{
// TODO: Futuramente vamos implementar estas validações
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
await _mediator.Send(command);
return NoContent();
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao tentar atualizar um Feed: {@Command}", command);
throw;
}
}
[HttpDelete("{id}")]
[Authorize]
public async Task<IActionResult> Delete([FromRoute] DeleteFeed command)
{
try
{
await _mediator.Send(command);
return NoContent();
}
catch (KeyNotFoundException)
{
return NotFound();
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao tentar deletar um Feed: {@Command}", command);
throw;
}
}
}
}

Note que podemos utilizar os attributes [FromRoute] e {FromBody] para que o próprio framework mapeie o command/query para nós, sem precisarmos adicionar vários parâmetros!

Antes de executar a API precisamos fazer mais uma configuração: Adicionar o MediatR à injeção de dependência. Podemos fazer isso dentro do nosso Startup.cs :

https://medium.com/media/3dd9e3eaee303db66a5dc06752ee2570/href

Finalmente, vamos executar a API e dar uma olhada no nosso Swagger!

Explorando os métodos que criamos para o FeedsController

Problemas de Serialização

Para os feeds não tivemos nenhum problema ao serializar nossos json. Mas, ao seguirmos para tags e contents, acabaremos com um problema, ao tentar retornar tags, por exemplo: System.Text.Json.JsonException: A possible object cycle was detected which is not supported.

O erro sendo retornado no Console da API

Este erro ocorre pois ao retornarmos uma Tag ela referencia o Feed ao qual pertence que, então, referencias suas Tags … E assim vamos. O serializador padrão do .NET Core 3.1, System.Text.Json , detecta isso e impede a serialização. Porém, para quem já utilizou as versões anteriores do .NET Core, existe um outro serializador capaz de lidar com estes loops: Newtonsoft.Json.

Para adicionar o Netwonsoft.Json a um projeto .NET Core 3.1 precisamos:

  1. Adicionar o package Microsoft.AspNetCore.Mvc.NewtonsoftJson
  2. Configurar para que framework MVC utilize o Newtonsoft
  3. Opcionalmente: Indicar ao Swashbuckle para utilizar o Newtonsoft, adicionando o package Swashbuckle.AspNetCore.Newtonsoft https://medium.com/media/6830982a0f0db0a7b9e250f601fdd3c8/href

Considerações finais

No próximo post irei fazer um refactor no nosso código adicionando duas novas bibliotecas: AutoMapper e FluentValidations. O AutoMapper nos auxiliará a converter os Commands e Queries para nossos Models e o FluentValidation nos auxiliará a realizar a validação dos Commands e Queries através de uma api Fluent.

Obrigado por lerem mais este post e até a próxima! Abraços!

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more