Introdução ao MediatR

Ao trabalhar com CQRS, o primeiro passo é criar a infraestrutura de mensagens para suportar os comandos, consultas e eventos que a aplicação tenha. Somente a partir daí é que passamos a nos preocupar com o código referente as atividades quais nossa aplicação irá desempenhar. Para evitar a criação de toda estra estrutura, podemos recorrer à uma biblioteca chamada MediatR, que já disponibiliza esta estrutura para tráfego das mensagens, incluindo a possibilidade de notificações (eventos).

O vídeo abaixo explora essa biblioteca, com exemplos simples e práticos de sua utilização. O cenário é uma loja de comércio eletrônico extremamente simples, mas que faz uso de vários recursos que a biblioteca possui, passando pelas mensagens, tratadores, notificações e customização do pipeline de execução. O código usado no vídeo está disponível neste endereço.

Exceções dentro de Recursos Voláteis

O .NET Framework já fornece nativamente suporte para a criação de contextos transacionais através da classe TransactionScope. Além da possibilidade de alistar automaticamente recursos como base de dados, message queue, etc., é possível customizar e alistar também recursos voláteis, onde podemos customizar e proteger algum código que queremos que seja efetivado quando ocorrer o commit, ou desfeito se o rollback ocorrer.

Para essa customização, basta implementarmos a interface IEnlistmentNotification, que fornece os métodos específicos e permite ao recurso interceptar cada um dos momentos definidos pelo protocolo 2PC (two phase commit): prepare, commit e rollback. Uma implementação simples desta interface é exibida abaixo. É importante notar que dentro do construtor já avaliamos se a classe está sendo instanciada dentro de um ambiente transacionado, e se sim, já alistamos o recurso para ser notificado quando os respectivos eventos ocorrer.

public class Teste1 : IEnlistmentNotification
{
    public Teste1()
    {
        if (Transaction.Current != null)
            Transaction.Current.EnlistVolatile(
                this, EnlistmentOptions.EnlistDuringPrepareRequired);
    }

    public void Commit(Enlistment enlistment) { }

    public void InDoubt(Enlistment enlistment) { }

    public void Prepare(PreparingEnlistment preparingEnlistment) { }

    public void Rollback(Enlistment enlistment) { }
}

Os métodos são autoexplicativos. Mas vamos focar no método Prepare. Este método é disparado durante a primeira fase do protocolo 2PC, onde o gerenciador interrogando o recurso se pode ou não realizar o commit. É importante falarmos sobre o disparo de exceções aqui. É importante envolvermos todo o código que está dentro deste método em um bloco try/catch, pois se houver uma exceção não tratada aqui, isso fará com que o .NET aborte os métodos de rollbacks de outros recursos que estejam também alistados. Imagine que o método Rollback acima esteja implementado da seguinte forma:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
    throw new Exception("Erro no Teste1.");
}

Para provar que o problema de fato ocorre, vamos criar uma segunda classe que implemente esta mesma interface, e em seu método Rollback, simplesmente vamos escrever uma mensagem na tela. Conside o código abaixo, que instancia as duas classes (ambas implementam a interface em discussão aqui), e quando o método Prepare é chamado sobre o objeto teste1 disparando a exceção não tratada, o .NET aborta o resto e, consequentemente, o método Rollback do objeto teste2 não é disparado.

using (var transaction = new TransactionScope())
{
    var teste1 = new Teste1();
    var teste2 = new Teste2();

    transaction.Complete();
}

O comportamento que esperamos não é este, ou seja, temos também que garantir que o Rollback seja disparado com sucesso nos demais objetos que estão a seguir e fazem parte do mesmo contexto transacional. Para resolvermos isso, basta proteger o código do método Prepare em um bloco try/catch, e se algum problema ocorrer, basta chamar o método ForceRollback, indicando ao runtime do .NET que a transação deve ser desfeita. E ainda, se quiser prover informações adicionais sobre o problema ocorrido, basta capturar a exceção e informando-a para um overload deste mesmo método, que passa adiante o problema (via InnerException), protegendo toda stack trace e entregando a mensagem real do problema. Sendo assim, a implementação final é:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
    try
    {
        throw new Exception("Erro no Teste1");

        preparingEnlistment.Prepared();
    }
    catch (Exception ex)
    {
        preparingEnlistment.ForceRollback(ex);
    }
}

Ofuscando Dados com DataMask

Quando a nossa aplicação lida com dados sigilosos, precisamos nos atentar em como ela armazena isso fisicamente. Se alguém não autorizado tem acesso à base de dados, ele poderá visualizar todos os dados que estão lá. Uma opção que temos é armazenar os dados criptografados, mas nesta situação,  temos que nos preocupar como a chave de criptografia se quisermos reverter o valor para utilizá-lo novamente.

Imagine que o cliente faça uma compra no site, e depois que ele informa o cartão de crédito, queremos armazenar ele para melhor a experiência sua usuário, e assim, na próxima compra, o cartão já estará armazenado, não precisando informar novamente. Por mais que a aplicação não exponha diretamente o número do cartão de crédito em suas páginas/telas, se houver alguma vulnerabilidade, é possível ter acesso à tabela e, consequentemente, ao número de todos os cartões que estiverem lá.

O SQL Server 2016 possui um recurso chamado data mask, que como o próprio nome sugere, permite ofuscar a informação de uma determinada coluna para um determinado login, e por mais que se explore a vulnerabilidade, o SELECT sempre retornará o dado aplicando a mascará que você configurou. No script abaixo, alteramos a coluna CartaoDeCredito para exibir apenas os dois primeiros e os dois últimos números do cartão. Há ainda outras funções predefinidas, que retornam o valor padrão do tipo de dado da coluna, uma que substitui o endereço de e-mail por asteriscos, entre outras.

ALTER TABLE Cliente
    ALTER COLUMN CartaoDeCredito 
        ADD MASKED WITH (FUNCTION = 'partial(2, "**-****-****-**", 2)')
GO

Para o exemplo, criei um login limitado chamado WebUser, concedendo a permissão para realizar SELECT na tabela de clientes. Agora note que abaixo estamos utilizado ADO.NET para extrair o número do cartão de crédito para apresentar na tela. Quando utilizamos a string de conexão com acesso irrestrito, o cartão de crédito é exibido integralmente; se utilizarmos a string de conexão com o usuário WebUser, o cartão é apresentado com uma porção de asteriscos.

static void Main(string[] args)
{
    const string conexao1 = "Data Source=.;Initial Catalog=DB;user id=WebUser;password=123";
    const string conexao2 = "Data Source=.;Initial Catalog=DB;Integrated Security=True";

    Exibir(conexao1);
    Exibir(conexao2);
}

private static void Exibir(string connString)
{
    using (var conn = new SqlConnection(connString))
    {
        using (var cmd = new SqlCommand("SELECT CartaoDeCredito FROM Cliente", conn))
        {
            conn.Open();

            using (var dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
                if (dr.Read())
                    Console.WriteLine(dr.GetString(0));
        }
    }
}

E o resultado é:

12-****-****-78
1234-5678-1234-5678

Recursos do T-SQL

Assim como uma parte dos desenvolvedores, acabo também tendo acesso à bases de dados SQL Server. Como a aplicação utiliza ORM para persistência das informações, o código SQL é totalmente gerado pelo NHibernate. Enquanto o ORM é responsável por fazer a persistência, tenho a necessidade que recorrer ao T-SQL para manipular e gerar massas de dados para exibir (telas e relatórios) nesta mesma aplicação.

À medida em que a versão do SQL Server avança, além das funcionalidades que são criadas em nível ferramental, a sua linguagem também ganha novos recursos, para tornar cada vez mais fácil, otimizado e inteligente a extração e manipulação de dados. Muitas vezes deixamos de acompanhar essa evolução, e quando você se dá conta, há diversos novas opções que facilitam a forma como lidamos com os dados; algo que às vezes, você até recorre à uma linguagem genuína (C#, por exemplo) para tentar realizar uma atividade, pois o T-SQL é “limitado”. A finalidade deste pequeno artigo, é demonstrar alguns recursos, que não são necessariamente novos, mas que podem facilitar o nosso dia-à-dia. Todos os scripts estão disponíveis neste endereço.

PIVOT: Possibilidade de tornar linhas em colunas, agrupando (MAX, MIN, SUM, etc.) os valores que deseja.

SELECT
	*
FROM
(
	SELECT
		  MONTH(p.Data) As Mes
		, SUM(Valor) As Total
	FROM Pedido p
	WHERE YEAR(p.Data) = 2016
	GROUP BY MONTH(p.Data)
) sq
PIVOT
(
	SUM(Total)
	FOR Mes IN ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12])
) pvt

ROW_NUMBER: Permite numerar as linhas do resultado. Há a opção com PARTITION, que como o próprio nome diz, particiona o contador dado um critério de ordenação.

SELECT
	  p.Data
	, p.Valor
	, ROW_NUMBER() OVER (PARTITION BY MONTH(Data) ORDER BY Data ASC) As Linha
FROM Pedido p

PAGINAÇÃO: Se a aplicação suportar, a consulta pode separar em páginas e exibir parte do resultado, que é justamente aquilo que o usuário consegue ver de uma única vez.

DECLARE @PageSize As Int = 10
DECLARE @PageNumber As Int = 1

SELECT 
	  Data
	, Valor
FROM Pedido
ORDER BY Data
	OFFSET @PageSize * (@PageNumber - 1) ROWS
	FETCH NEXT @PageSize ROWS ONLY;

WITH TIES: Quando especificamos a cláusula TOP (N), ele retorna exatamente o número de registros que foi especificado. Com esta opção e dependendo do critério de ordenação, ele acaba extrapolando o número de itens do TOP entendendo que os registros que possuem o mesmo valor da ordenação, devem também estar sendo incluídos no resultado.

SELECT TOP 5 WITH TIES
	*
FROM Pedido
ORDER BY MONTH(Data) DESC

BULK INSERT: Se possui um arquivo do tipo CSV, é possível utilizar este recurso para interpretar o mesmo e conseguir inserir diretamente em uma determinada tabela da base de dados.

Id;Data;Valor;Pago
;2016-03-30 00:00:00;14000.00;0
;2016-04-30 00:00:00;14000.00;1
;2016-05-30 00:00:00;14000.00;1

BULK INSERT Pedido
FROM 'C:\Temp\05 - BulkInsert.csv'
WITH
(
	FIRSTROW  = 2,
	FIELDTERMINATOR = ';',
	ROWTERMINATOR = '\n'
);

IIF: Operador ternário. Simplifica o uso excessivo de CASE WHEN, que às vezes acaba tornando a consulta ilegível.

SELECT
	  Data
	, Valor
	, IIF(Pago = 1, 'Sim', 'Não') As Pago
FROM Pedido

AGREGAÇÃO COM WINDOW FUNCTION: Permite aplicar uma regra de agregação em partes do resultado, dado um critério de particionamento do mesmo. No exemplo abaixo, a ideia é ir criando totalizações linha a linha, com a soma de tudo o que está acima da linha corrente.

SELECT
	  Data
	, Valor
	, SUM(Valor) OVER (ORDER BY Data ASC) As Total
FROM Pedido
ORDER BY Data ASC

TABLE VALUED PARAMETERS: Um novo tipo de dados que simula um array de um determinado tipo. Permite a parametrização da cláusula IN. Pode ser utilizado nativamente com o ADO.NET.

CREATE TYPE RelacaoDeIDs AS TABLE 
(
	Id Int NOT NULL
)
GO

DECLARE @Ids As RelacaoDeIds
INSERT INTO @Ids VALUES (3)
INSERT INTO @Ids VALUES (34)
INSERT INTO @Ids VALUES (48)

SELECT * FROM Pedido WHERE Id IN (SELECT Id FROM @Ids)

CONSULTA COM EXPRESSÃO REGULAR: Utilizando uma expressão regular para retornar os clientes onde o nome termina com algum número.

SELECT * FROM Cliente WHERE Nome LIKE '%[0-9]'

LAG/LEAD: Funções que permite acessar alguma coluna da linha anterior ou posterior, respectivamente.

-- Retorna NULL
SELECT
	  Data
	, Valor
	, LAG(Data, 1) OVER(ORDER BY Data ASC) As PedidoAnteriorEm
FROM Pedido 
ORDER BY Data ASC

-- Retorna 2016-01-02 00:00:00.000
SELECT
	  Data
	, Valor
	, LEAD(Data, 1) OVER(ORDER BY Data ASC) As ProximoPedidoEm
FROM Pedido 
ORDER BY Data ASC

WITH CTE: Caso você precise de uma massa de dados múltiplas vezes, você pode incluir este resultado em uma CTE (Common Table Expressions) e reutilizar a mesma. Consultas recursivas (exemplo: empregado com superior, itens de menu, etc.), também podem usufruir deste recurso.

WITH totalizacao
As
(
	SELECT
		SUM(Valor) As Valor
	FROM Pedido
)
SELECT
	  Data
	, Valor
	, ROUND(Valor / (SELECT Valor FROM totalizacao) * 100, 2) As Percentual
FROM Pedido p

FORMATMESSAGE: Opção para formatar strings sem ter que ficar utilizar o caractere de concatenação (+).

SELECT
	  Data
	, Valor
	, FORMATMESSAGE('Status: %s.', IIF(Pago = 1, 'Pago', 'Não Pago')) As Pagamento
FROM Pedido

EXCLUSÃO DE OBJETOS: Sintaxe mais simples para remoção de objetos.

-- Antes
IF OBJECT_ID('Pedido', 'U') IS NOT NULL
	DROP TABLE Pedido

-- Agora
DROP TABLE IF EXISTS Pedido

STRING_SPLIT: Função que recebe uma string (podendo ser uma coluna de uma tabela) separada por um determinado caractere e permite utilizar o resultado na cláusula FROM ou JOIN.

SELECT value FROM string_split('CQRS;HTTP;Data', ';')

COMPRESSÃO: Opção nativa para compressão e descompressão de dados utilizando o varbinary.

INSERT INTO Pedido VALUES (COMPRESS('Dados da Nota Fiscal 123'))

SELECT CONVERT(Varchar(100), DECOMPRESS(NotaFiscal)) FROM Pedido

Validação de Comandos

No artigo/vídeo anterior eu comentei como podemos expor comandos através do protocolo HTTP. O comando é serializado em JSON e enviado para o servidor que tenta encontrar a respectiva classe que representa o comando e, consequentemente, executa o seu respectivo handler para tratar a requisição. E como sabemos, os comandos são classes extremamente simples, contendo apenas poucas propriedades que parametrizam a tarefa a ser executada.

Só que os comandos podem chegar até o serviço sem as informações mínimas para a sua execução. Apesar de estarmos falando em comunicação entre sistemas, isso se aproxima muito do que temos em uma aplicação com interface com o usuário: apesar de termos uma validação sendo realizada no formulário, é uma boa prática reaplicar as validações quando ele é postado, já que do lado do cliente, a validação pode ser desabilitada, como é o caso de aplicações Web, que recorrem à JavaScript, que com algum esforço, podemos inativar no navegador e postar a mensagem sem devidas validações. A validação do lado do cliente serve para dar uma melhor experiência para o usuário, dando o feedback se alguma informação estiver inválida antes da requisição partir.

Isso é muitas vezes chamadas de validação superficial, e ela não substitui aquela que é feita pelo domínio, que por sua vez, é a qual garante a consistência da aplicação. Essa validação nos comandos devem garantir os parâmetros obrigatórios, que itens não sejam nulos ou iguais a zero, formatação de campos (e-mail, URLs, etc.), etc.

Agora, se a ideia é expor os comandos através de HTTP, temos também que tentar integrar as validações com os recursos que o protocolo fornece. Só que antes disso, precisamos incorporar nas classes dos comandos a estrutura da validação. Aqui existem diversas formas de se fazer, onde a mais comum, é declarar na classe base que representa o comando um método virtual que pode ser sobrescrito nas derivadas para customizar a validação de cada um deles.

public abstract class Command
{
    public virtual bool Validate() { }
}

A implementação do método pode ser feito de diversas formas, e uma delas sendo a forma “manual”, utilizando apenas recursos (tipos) da linguagem e criando classes que representam os erros. O método Validar retorna apenas um indicativo (bool) se a validação sucedeu ou não. Mas o que falhou? E vamos optar por retornar apenas quando a primeira falha for encontrada ou validaremos todo o objeto e retornaremos todos os problemas encontrados? Neste caso, ao invés do método retornar apenas um bool, teremos que customizar o tipo de retorno, com uma classe que totaliza todos os problemas encontrados.

public abstract class Command
{
    public virtual ValidationResult Validate()
    {
        return ValidationResult.Empty;
    }
}

public class ValidationResult
{
    public static ValidationResult Empty = new ValidationResult();
    private readonly IList<Error> errors = new List<Error>();

    public void Add(Error error) => this.errors.Add(error);

    public bool IsValid
    {
        get
        {
            return !this.errors.Any();
        }
    }

    public IEnumerable<Error> Errors
    {
        get
        {
            return this.errors;
        }
    }

    public class Error
    {
        public string Message { get; set; }
    }
}

A outra forma seria recorrer à algum framework de validação, como o FluentValidation, que já fornece diversos recursos para facilitar a validação. Depois disso, precisamos de alguma forma integrar com o HTTP, e a opção que temos é retornar o erro 400 (BadRequest), que serve justamente para representar alguma falha encontrada na postagem da requisição (onde o seu conteúdo não é válido). A resposta deve conter os dados da validação, para que o cliente consiga entender o problema e corrigir para as futuras requisições.

Por fim, ao receber a requisição, encontrar o respectivo comando e deserializar a mensagem, invocamos o método Validate e avaliamos a propriedade IsValid. Caso seja negativo, então configuramos o código de retorno do HTTP para 400 (BadRequest) e serializamos o resultado da validação  em JSON para ser devolvido ao cliente; caso o resultado da validação seja positivo, então a requisição é encaminhada para ser normalmente processada. O código na integra está disponível neste endereço.

var command = context.Request.ReadAsCommand(bus.Handlers[action]);
var validateResult = command.Validate();

if (!validateResult.IsValid)
{
    context.Response.StatusCode = 400;
    await context.Response.WriteAsync(validateResult.ToJson());
    return;
}

await bus.Send(command);

É importante dizer que isso que falamos aqui é uma validação superficial, e não deve ser utilizada sozinha, é só um complemento. O comando essencialmente não retorna resultado, mas a validação dele é algo que ocorre antes de sua execução. Agora se algo der errado durante a execução dele (pois está “tocando” o domínio, que pode estar protegido com o disparo de exceções), é muito provável que seja alguma regra de negócio que tenha sido violada, e para isso, há outras estratégias de notificação para reportar ao usuário ou à outras aplicações destas falhas, como o uso de eventos.

Web Commands

Quando falamos de CQRS, temos as consultas e os comandos. Esses comandos podem ser consumidos diretamente por aplicações, mas como expor publicamente para os clientes? Uma opção comum nos dias de hoje é utilizar o protocolo HTTP, facilitando assim o consumo pelas mais diversas linguagens e tecnologias. E quais as opções no .NET para fazermos esta exposição?

O ASP.NET é um candidato para isso, já que em sua versão core está modularizado e flexível, e assim podemos otimizar o pipeline de execução com somente aquilo que seja de fato necessário para executar os comandos. A finalidade do vídeo abaixo é exibir uma forma alternativa para incorporar os comandos do domínio no ASP.NET, utilizando o mínimo de recursos possível do hosting. Link para o código do exemplo.

Expondo Coleções

Quando alguma classe que criamos internamente mantém uma coleção, é comum termos um método público que permite a inclusão de novos itens na mesma. Geralmente se faz isso ao invés de expor diretamente a coleção, já que através deste método podemos interceptar cada item que está sendo adicionado, validar e também tomar decisões e fazer totalizações sobre cada um deles antes de efetivamente incluir na coleção.

Para exemplificar o que estou falando, considere o exemplo abaixo. Note que a cada inserção de um novo item através do método Adicionar, ele armazena na propriedade Total da classe Pedido o valor unitário do produto multiplicado pela quantidade de itens comprados.

public class Pedido
{
    private readonly IList<Item> itens = new List<Item>();

    public void Adicionar(Item item)
    {
        this.itens.Add(item);

        this.Total += item.Quantidade * item.Valor;
    }

    public IEnumerable<Item> Itens
    {
        get
        {
            return this.itens;
        }
    }

    public decimal Total { get; set; }
}

Além dos benefícios que disse acima, este código, teoricamente, não permite o acesso direto à coleção interna, e sendo assim, qualquer adição tem que passar pelo método Adicionar. Só que isso não procede. Note que temos uma propriedade chamada Itens que retorna os itens através do tipo IEnumerable. Isso é possível porquê a coleção implementa esta interface. Porém, se o consumidor que estiver acessando esta propriedade fizer a conversão para IList, então o resultado não será o que desejamos:

var pedido = new Pedido();
pedido.Adicionar(new Item() { Quantidade = 2, Valor = 23 });

var lista = pedido.Itens as IList<Item>;
lista.Add(new Item());

Console.WriteLine(pedido.Itens.Count()); //Retornará 2 itens

Para evitar que isso ocorra, basta no interior da propriedade Itens chamar o método de extensão ToList, ou instanciar a classe ReadOnlyCollection (System.Collections.ObjectModel), conforme os exemplos abaixo, evitando assim, que o consumidor modifique a coleção, conforme ocorreu no exemplo anterior.

//Exemplo 1
public IEnumerable<Item> Itens => itens.ToList();

//Exemplo 2
public IEnumerable<Item> Itens
    => new ReadOnlyCollection<Item>(itens);