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);

Repositórios Plug-and-Play

No artigo anterior falamos sobre o uso de JOINs para extrair as informações que armazenamos em uma base de dados relacional. Comentamos também das dificuldades que temos quando precisamos manter os dados originais do momento da execução daquela tarefa (exemplo: o nome do cliente no momento da emissão da nota fiscal). No fim deste mesmo artigo, falamos em segregação da base de dados, a fim de ter estruturas distintas e otimizadas para leitura e gravação.

Dando continuidade no artigo, vamos falar sobre a base de dados de escrita. Quando trabalhamos com um domínio rico, baseado em DDD, é comum nossas classes possuírem diversas propriedades, que representam suas características, bem como funcionalidades e comportamentos que refletem, em geral, as mesmas funções do mundo real. Uma vez que este domínio está desenvolvido, chega o momento de decidir como vamos persisti-lo fisicamente. E a resposta na maioria das vezes, a resposta sempre é o uso de uma base de dados relacional, tais como: SQL Server, Oracle, MySQL, etc.

O problema entre o nosso domínio e uma base de dados relacional é a impedância. Isso quer dizer, o atrito que temos ao mapear classes e propriedades para tabelas, colunas e linhas. Trata-se de um trabalho árduo, e muitas vezes recorremos à mapeadores (ORM) para auxiliar nesta atividade, e que apesar de fazerem um grande trabalho por convenção, há muito o que se customizar em um ambiente mais complexo. Isso sem falar sobre evolução do domínio, onde a criação de novos tipos, adição e remoção de novas propriedades que precisam ser armazenadas, refletirá no mapeamento e que deverá ser ajustado.

Por essa flexibilidade na evolução e diversos outros pontos positivos, é que bases NoSQL estão cada vez mais ganhando espaço. E aqui, vamos tentar fazer a migração de uma aplicação que faz uso de NHibernate para RavenDB. Como estamos trabalhando orientado ao domínio, nós já temos interfaces em nosso código que definem a estrutura dos repositórios que a mesma utiliza. Por padrão, temos apenas a implementação para o NHibernate:

public class RepositorioDeClientes : IRepositorioDeClientes
{
    private readonly ISession session;

    public RepositorioDeClientes(ISession session)
    {
        this.session = session;
    }

    public void Salvar(Cliente entidade)
    {
        session.Save(entidade);
    }

    public Cliente BuscarPor(string cnpj)
    {
        return
            (
                from c in session.Query<Cliente>()
                where c.Empresa.Cnpj == cnpj
                select c
            ).SingleOrDefault();
    }
}

Só que o repositório ainda depende do arquivo de mapeamento (HBM) para fazer o de/para das classes para as tabelas. Vou omitir o mapeamento aqui. Ele estará disponível no código fonte do projeto que estará relacionado à este artigo. Como a interface define a estrutura, criamos uma implementação do repositório para o RavenDB. É possível notar que a API do ORM é bem semelhante à API do RavenDB.

public class RepositorioDeClientes : IRepositorioDeClientes
{
    private readonly IDocumentSession session;

    public RepositorioDeClientes(IDocumentSession session)
    {
        this.session = session;
    }

    public void Salvar(Cliente entidade)
    {
        session.Store(entidade);
        session.SaveChanges();
    }

    public Cliente BuscarPor(string cnpj)
    {
        return
            (
                from c in session.Query<Cliente>()
                where c.Empresa.Cnpj == cnpj
                select c
            ).SingleOrDefault();
    }
}

Se notarmos os códigos que criam um novo cliente utilizando os repositórios criados acima, eles serão idênticos, exceto pela criação, que cada um deve recorrer ao seu próprio modelo de construção e conexão. Se você injeta o repositório em outras classes, elas continuarão funcionando de forma transparente, já que toda a “complexidade” está embutida no repositório, e sua interface continua expondo os mesmos métodos.

public static class Repositorios
{
    public static void Executar()
    {
        ViaNHibernate();
        ViaRavenDB();
    }

    private static void ViaNHibernate()
    {
        using (var session = NHibernateContext.Factory.OpenSession())
        {
            var repositorio = new R.NHibernate.RepositorioDeClientes(session);
            var cliente = new Cliente(new Empresa() { RazaoSocial = "Nome Ltda", Cnpj = "123" });

            repositorio.Salvar(cliente);

            Console.WriteLine(repositorio.BuscarPor(cliente.Empresa.Cnpj).Empresa.RazaoSocial);
        }
    }

    private static void ViaRavenDB()
    {
        using (var session = RavenDBContext.Store.OpenSession())
        {
            var repositorio = new R.RavenDB.RepositorioDeClientes(session);
            var cliente = new Cliente(new Empresa() { RazaoSocial = "Nome Ltda", Cnpj = "123" });

            repositorio.Salvar(cliente);

            Console.WriteLine(repositorio.BuscarPor(cliente.Empresa.Cnpj).Empresa.RazaoSocial);
        }
    }
}

E como uma das principais características de bases NoSQL é não ter um schema predefinido, não há nada que se precise fazer para estruturar a base antes de receber os dados. E com isso, todos os arquivos HBM de mapeamento do NHibernate (ou se usar o Fluent NHibernate), podem ser completamente descartados. O método BuscarPor está sendo chamado aqui apenas para gerar o round-trip de testes.

Relacionamentos

Quando trabalhamos com base de dados relacional, utilizamos os relacionamentos para referenciar em uma linha o conteúdo de outra, ou seja, para complementar a informação, e evitando com isso, a redundância de dados. Para dar um exemplo, quando um pedido é realizado, ao invés de armazenar nos itens do mesmo toda a informação acerca do produto comprado, simplesmente referenciamos o produto através de seus Id como uma chave estrangeira, e através de JOINs, conseguimos remontar o pedido e saber quais produtos foram comprados.

Como mencionei no artigo anterior, além do custo de executar um JOIN, muitas vezes a redundância é benéfica. E vale ressaltar novamente aqui: o custo de armazenamento, via de regra, é mais barato que o custo de processamento. Ao mudar para uma base de dados NoSQL, os relacionamentos são tratados de forma um pouco diferente aos quais conhecemos no mundo relacional. Abaixo vou mostrar alguns experimentos, e entender o comportamento de cada um deles para podemos escolher o qual melhor se encaixa com a nossa necessidade.

Para o exemplo, vamos considerar um exemplo bastante trivial: uma classe chamada Pedido, que possui os itens que foram comprados. Para representar cada item, teremos a classe Item, que, em princípio, referenciará o produto (classe Produto) que foi adquirido. Essa classe irá ser modificada para exemplificar cada um dos cenários. Por fim, a classe Pedido irá expor uma propriedade chamada Itens que retorna todos os itens associados à ele.

Flat

A primeira opção é conhecida como flat, ou seja, as referências serão armazenadas como parte do documento principal, embutindo todas as propriedades e seus respectivos valores. Para este primeiro cenário, a estrutura da classe Item do pedido será a seguinte:

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.Produto = produto;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public Produto Produto { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

Note que a instância da classe produto é passada como parâmetro no construtor da classe Item e armazenado em uma propriedade. O RavenDB irá embutir todas as propriedades do produto no interior do documento pedido. Como um paralelo, o NHibernate permite configurar a propriedade cascade para “save-update” e fazer com que a classe Produto seja salva em uma tabela exclusiva para o mesmo; mais tarde, se este produto for novamente comprado, então ela reutilizaria o mesmo registro. Abaixo o código que utilizamos para inserir o pedido e o JSON que foi armazenado na base:

var repositorioDePedidos = new RepositorioDePedidos(RavenDBContext.Store.OpenSession());

var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };
var pedido = new Pedido();

pedido.AdicionarItem(new Pedido.Item(produto, 2));

repositorioDePedidos.Salvar(pedido);
{
    "Data": "2017-01-17T16:59:58.2372992",
    "Total": 240,
    "Itens": [
        {
            "Produto": {
                "Descricao": "Mouse Microsoft",
                "Valor": 120,
                "Id": null
            },
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

O grande ponto negativo deste modelo, é que se precisarmos alterar qualquer informação no produto, teremos que varrer todos os pedidos existentes e fazer a tal alteração.

PorId

A opção por id faz com que a referência para o produto seja armazenada, ou seja, o item passa agora a referenciar o id do produto que foi comprado que, por sua vez, e acaba sendo armazenado em uma coleção a parte dos pedidos. Com isso, os dados do produto não serão duplicados, mas você terá que saltar para outro documento para ter acesso às informações sobre o produto que foi comprado. A classe Item sofre uma pequena alteração para armazenar apenas o Id do produto:

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.ProdutoId = produto.Id;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public string ProdutoId { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

Agora o produto deve ser armazenado separadamente em nossa base, afinal, é muito provável que ele seja um agregado na perspectiva do domínio, o que o confere direito de ter um repositório. Obviamente que antes de comprarmos algo, ele deve estar previamente cadastrado, e é esta instância deste produto já cadastrado que é passada para o item do pedido, que o reutilizará:

using (var session = RavenDBContext.Store.OpenSession())
{
    var repositorioDePedidos = new RepositorioDePedidos(session);
    var repositorioDeProdutos = new RepositorioDeProdutos(session);

    var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };

    repositorioDeProdutos.Salvar(produto);

    var pedido = new Pedido();

    pedido.AdicionarItem(new Pedido.Item(produto, 2));

    repositorioDePedidos.Salvar(pedido);
}
{
    "Data": "2017-01-17T17:11:49.7687856",
    "Total": 240,
    "Itens": [
        {
            "ProdutoId": "produtos/1",
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

Redundante

Por fim, esta opção nos permite criar uma classe e copiar somente os campos necessários do objeto de origem. A ideia é trazer para o item tudo o que é necessário para realizar a compra, e mesmo que no futuro as informações mudem, o pedido atual terá armazenado as informações originais do momento da compra. Quando, por exemplo, a descrição do produto for alterada, não mais refletirá nos pedidos já realizados.

Para isso, a classe Item passa a armazenar também a descrição do produto, além do valor que já vinha fazendo. Não há mais qualquer referência com a classe Produto.

public class Item
{
    public Item(Produto produto, int quantidade)
    {
        this.Descricao = produto.Descricao;
        this.Quantidade = quantidade;
        this.Total = produto.Valor * quantidade;
    }

    public string Descricao { get; private set; }

    public int Quantidade { get; private set; }

    public decimal Total { get; private set; }
}

O código que armazena o pedido nada muda em relação ao que vimos no primeiro cenário, porém se inspecionarmos o JSON da base de dados, veremos que ele armazena apenas as propriedades da classe Item, sem qualquer menção ao Produto, muito menos ao seu Id.

using (var session = RavenDBContext.Store.OpenSession())
{
    var repositorioDePedidos = new RepositorioDePedidos(session);

    var produto = new Produto() { Descricao = "Mouse Microsoft", Valor = 120M };
    var pedido = new Pedido();

    pedido.AdicionarItem(new Pedido.Item(produto, 2));

    repositorioDePedidos.Salvar(pedido);
}
{
    "Data": "2017-01-17T17:21:39.6605485",
    "Total": 240,
    "Itens": [
        {
            "Descricao": "Mouse Microsoft",
            "Quantidade": 2,
            "Total": 240
        }
    ]
}

A utilização depende de cada situação que temos. Em muito casos quando trabalhamos com base de dados relacional, não nos atentamos para algumas situações, que podem gerar problemas mais sérios em termos de negócios, como por exemplo, não armazenar o nome ou o endereço do cliente no momento da realização da compra. No caso do RavenDB a redundância é natural, mas, como disse, nem sempre ela é ruim.

O código fonte dos exemplos utilizados no decorrer este artigo está disponível neste endereço.

Interface IApplicationLifetime

Desde o ASP clássico nós temos um arquivo chamado Global.asa. Com o ASP.NET, este arquivo foi renomeado para Global.asax. A finalidade deste arquivo é fornecer uma forma de interceptar eventos que ocorrem no nível da aplicação. Entre estes eventos, nós temos um que é disparado indicando que a aplicação foi inicializada, outro que algum erro não tratado aconteceu, que uma nova requisição chegou, que a aplicação está sendo encerrada, etc.

Este arquivo já não existe mais no ASP.NET Core, porém a necessidade ainda existe em alguns tipos de aplicação. Para interceptar a inicialização da aplicação e o término dela, podemos recorrer à interface IApplicationLifetime. Esta interface fornece três propriedades que indica a inicialização da aplicação (ApplicationStarted), que ela está sendo encerrada (ApplicationStopping) e depois de encerrada (ApplicationStopped). Essas propriedades são do tipo CancellationToken, e através de um delegate do tipo Action, passamos para o método Register o código customizado que queremos executar quando estes eventos acontecerem.

Para exemplificar o uso e utilidade desta interface, considere o exemplo abaixo onde temos uma classe estática que gerencia o agendamento e execução de tarefas pelo Quartz.NET. Além do método Schedule, temos os métodos Start e Stop, que como já podemos perceber, queremos invocar cada um deles no início e no fim da aplicação.

public class TaskManager
{
    public static void Start()
    {
        Scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;
        Scheduler.Start();
    }

    private static IScheduler Scheduler { get; set; }

    public async static Task Schedule<TJob>(IDictionary data) where TJob : IJob
    {
        await Scheduler.ScheduleJob(
            JobBuilder
                .Create<TJob>()
                .SetJobData(new JobDataMap(data))
                .Build(),
            TriggerBuilder
                .Create()
                .WithSimpleSchedule(x => x.WithRepeatCount(0))
                .Build());
    }

    public static void Shutdown()
    {
        Scheduler?.Shutdown(true);
    }
}

O método Start deverá ser chamado quando a aplicação estiver sendo inicializada. Já no momento, do encerramento, recorremos ao método Shutdown do Schedule, passando o parâmetro true, que indica que antes de encerrar o gerenciador de tarefas, é necessário aguardar o término de alguma que, eventualmente, ainda esteja em execução. A interface IApplicationLifetime, assim como diversas outras do ASP.NET Core, o próprio runtime cria a instância de uma implementação padrão e passa para o método Configure da nossa classe Startup. Com isso, basta associarmos os métodos aos respectivos eventos, conforme é possível visualizar abaixo:

public void Configure(
    IApplicationBuilder app, 
    IHostingEnvironment env, 
    ILoggerFactory log, 
    IApplicationLifetime lifetime)
{
    lifetime.ApplicationStarted.Register(() => TaskManager.Start());
    lifetime.ApplicationStopped.Register(() => TaskManager.Shutdown());

    //outras configurações
}

Por fim, a aplicação (os controllers) interagem com a classe TaskManager a partir do método Schedule<TJob>, indicando qual a tarefa que precisa ser executada e parâmetros contextuais que queremos passar para a implementação da mesma.

[HttpPost]
public async Task<IActionResult> AgendarExecucao()
{
    await TaskManager
        .Schedule<GeracaoDeDados>(
            Request.Form.ToDictionary(x => x.Key, x => x.Value.ToString()));

    return View("ExecucaoAgendada");
}