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

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.

Além dos Tipos Primitivos

Ao criar tipos customizados para a aplicação, é comum utilizarmos os tipos primitivos da linguagem (inteiro, data, decimal, etc.) para especificar os campos que ele deverá conter, podendo trabalhar internamente ou externamente com estas informações. Se não nos atentarmos no momento da criação, é capaz de inflarmos o novo tipo com uma porção de propriedades, que muitas vezes podem estar relacionadas, e prejudicando assim a interface exposta para os consumidores.

Além disso, podemos perder parte do encapsulamento, pois se deixarmos para o consumidor combinar os campos para testar uma condição, pode ser que em algum momento ele se esqueça de avaliar de forma correta e, consequentemente, ter um resultado inesperado. Para exemplificar, considere a classe abaixo:

tipos1

Note que as propriedades estão divididas nas cores vermelha e azul. As propriedades em vermelho dizem a respeito do vencimento do boleto, enquanto as propriedade em azul se referem ao valor do título. O valor a ser pago pelo título é o valor bruto deduzindo o abatimento e o desconto (desde que esteja sendo pago antes da data limite para o mesmo). Já o vencimento agrupa tudo o que é necessário para indicar a real data de pagamento e/ou a quantidade de dias que falta para vencer ou que está vencido.

Se o consumidor quiser saber a data real de vencimento terá que fazer esta análise; se ele quiser saber o valor real do pagamento (deduzindo os descontos), terá que calcular. E o pior, isso deverá ser replicado para todos os pontos da aplicação que precisar disso. Uma solução seria criar propriedades de somente leitura na classe Boleto e já retornar os valores calculados. Só que fazendo isso, voltamos ao problema inicial: comprometer a interface pública do tipo. Mesmo que estivermos confortáveis em prejudicar a interface, pode ter outros tipos dentro do nosso domínio que precisa também lidar com isso (uma nota promissória, por exemplo, também tem vencimento), e tornamos a recriar tudo isso lá.

Para centralizar as regras, padronizar o consumo e reutilizar quando necessário, o ideal é abrir mão dos tipos primitivos e passar a criar tipos específicos para o nosso domínio, que agrupe os campos e forneça os dados já calculados, para que seja possível o consumo unificado das informações.

tipos2

Depois disso, a nossa classe Boleto fica bem mais enxuta, e a medida que vamos se aprofundando nas propriedades, chegamos nos mesmos dados, dando mais legibilidade e tendo a mesma riqueza de informações que tínhamos antes.

public class Boleto
{
    public Vencimento Vencimento { get; set; }
    public Valor Valor { get; set; }
}

var boleto = new Boleto()
{
    Vencimento = DateTime.Now.AddDays(5),
    Valor = 1250.29M
};

Note que se ajustarmos os operadores (overloading), conseguimos converter tipos primitivos em tipos customizados, conforme é possível ver acima, e tornar ainda mais sucinto o código.

Por fim, os ORMs que temos hoje permitem a serialização de todo o tipo ou de parte dele. Fica a nosso critério o que armazenar, ou seja, se optamos apenas pelos dados que originam o restante ou se armazenamos tudo o que temos, mas não é uma decisão fácil de tomar. Se optar por armazenar tudo, você ocupará mais espaço em disco; em contrapartida, se guardar apenas os dados que originam o restante e utilizar algum outro mecanismo para extração dos mesmo (DAL) sem passar pelo domínio, você não conseguirá reconstruir algumas informações sem replicar a regra na sua camada de leitura.

Eventos de Domínio – Outra Opção de Disparo

Nos artigos anteriores falamos sobre a geração e consumo de eventos de domínio. Entre os assuntos abordados, discutimos os tratadores, que nada mais são que classes que são executadas reagindo ao evento que foi disparado. Ainda falando sobre os tratadores, abordamos a forma de descobrir os tratadores que fazem parte da aplicação (estática ou dinâmica) bem como a possibilidade de incluir novos tratadores em tempo de execução.

Para recapitular, temos a classe DomainEvents, qual utilizamos para disparar os eventos. Nos exemplos anteriores, esta classe estava sendo utilizada no interior das entidades, que quando era detectado a necessidade de disparo de algum evento, recorria ao método Raise, informando o tipo do evento e suas respectivas informações.

public void Lancar(Lancamento lancamento)
{
    var saldoAnterior = this.Saldo;

    this.lancamentos.Add(lancamento);
    this.Saldo += lancamento.Valor;

    DomainEvents.Raise(
        new SaldoDaContaAlterado(this.NomeDoCliente, saldoAnterior, this.Saldo));
}

O problema desta técnica é que a entidade além de criar o evento, também está sendo responsável por disparar ele, e se algum problema acontecer depois do disparo de evento que notifica a alteração do saldo, não é fácil desfazer o que já foi realizado pelo(s) tratador(es). Existem situações em que não dá para assegurar que depois do evento disparado as informações serão corretamente persistidas sem que algum erro ocorra. Considere o exemplo de código a seguir:

var repositorio = new RepositorioDeContas();

var cc = new ContaCorrente("Israel Aece");
cc.Lancar(new ContaCorrente.Lancamento("Pagto de Energia", -1000));

repositorio.Atualizar(cc);

Conforme vimos anteriormente, o método Lancar dispara o evento e o tratador adiciona o cliente para monitoramento. Imagine agora que ao invocar o método Atualizar do repositório, algum exceção ocorra. A complexidade para ir até o monitor e desfazer a inserção do cliente seria muito custosa e de difícil implementação. Isso poderia piorar ainda mais se estivermos trabalhando entre contextos distintos, que podem estar fisicamente separados.

Felizmente temos uma alternativa para melhorar a implementação e o disparo dos eventos, combinando isso com o repositório da entidade. Ao invés das entidades gerarem e dispararem os eventos, criamos internamente uma coleção destes eventos para que ela vá armazenando todos os acontecimentos, e ao atualizar na base de dados, percorremos todos os eventos, disparando cada um deles. Para uma melhor reutilização de código, criamos uma classe base para todas as entidades, ou melhor, para os aggregate roots.

public abstract class Entidade
{
    private readonly IList<IDomainEvent> eventos = 
        new List<IDomainEvent>();

    protected void AdicionarEvento(IDomainEvent evento)
    {
        this.eventos.Add(evento);
    }

    public void RemoverEventos()
    {
        this.eventos.Clear();
    }

    public IEnumerable<IDomainEvent> Eventos
    {
        get
        {
            return this.eventos;
        }
    }
}

Internamente esta classe armazenará a coleção de eventos, representado por instâncias de classes que implementam a interface IDomainEvent. A implementação do método Lancar tem uma suave mudança, e passa a recorrer ao método AdicionarEvento (que é protected) para adicionar o evento que indica a alteração do saldo.

public void Lancar(Lancamento lancamento)
{
    var saldoAnterior = this.Saldo;

    this.lancamentos.Add(lancamento);
    this.Saldo += lancamento.Valor;

    this.AdicionarEvento(
        new SaldoDaContaAlterado(this.NomeDoCliente, saldoAnterior, this.Saldo));
}

Isso por si só não funciona. Conforme falamos acima, temos que mudar o repositório para que ele identifique a existência de eventos e dos dispare. Mas aqui vale observar que ele somente deverá fazer isso depois que a atualização na base de dados (INSERT, UPDATE ou DELETE) seja realizada com sucesso. Os tratadores agora podem realizar suas atividades sem a preocupação de que aquilo poderia, em algum momento, ser desfeito.

Para manter a simplicidade, implementei o disparo dos eventos diretamente no repositório de contas, mas é possível refatorar o repositório a fim de criar uma base para todos os repositórios da aplicação, reutilizando o disparo de eventos para todas as entidades, já que o processo será o exatamente o mesmo. O método DispararEventos deve ser chamado sempre que a adição, atualização ou exclusão for realizada, e assim iteramos pela coleção de eventos (exposta pela classe abstrata Entidade) e invocamos o método estático Dispatch da classe DomainEvents. Por fim, depois dos eventos disparados, removemos os mesmos da entidade, já que se alguma coisa nova acontecer a partir dali, estes já estão concluídos.

public class RepositorioDeContas : IRepositorio<ContaCorrente>
{
    public void Atualizar(ContaCorrente entidade)
    {
        //Atualizar Base de Dados

        DispararEventos(entidade);
    }

    private static void DispararEventos(Entidade entidade)
    {
        foreach (var evento in entidade.Eventos)
            DomainEvents.Dispatch(evento);

        entidade.RemoverEventos();
    }
}

O método Dispatch tem funcionalidade semelhante ao Raise, mas soa melhor neste cenário, já que aqui ele tem a função de delegar o disparo dos eventos criados pelas entidades para os tratadores. Ao contrário do método Raise, que é genérico, o método Dispatch lida diretamente com instâncias da interface IDomainEvent ao invés dos eventos concretos. Por fim, ele analise se o tipo do evento que o tratador implementa é igual ao evento disparado, e o executa.

public static void Dispatch(IDomainEvent @event)
{
    foreach (var handler in handlers)
        if (handler.GetInterfaces()
                   .Any(h => h.IsGenericType && h.GenericTypeArguments[0] == @event.GetType()))
            ((dynamic)Activator.CreateInstance(handler)).Handle((dynamic)@event);
}

Eventos de Domínio – Disparo e Consumo

No artigo anterior falamos sobre a criação e utilização de eventos de domínio. O artigo abordou até o momento do disparo do evento propriamente dito, através da classe DomainEvents, só que sem mostrar detalhes de sua implementação. Existem diversas formas de se implementar o método de disparo do evento, mas antes de falarmos sobre estas técnicas, precisamos abordar como se constrói os consumidores dos eventos.

Para especificarmos os tratadores, vamos criar uma interface que descreverá apenas um método: Handler. Só que trata-se de uma interface genérica, onde o tipo T deve ser alguma classe que obrigatoriamente implemente a interface IDomainEvent, e que para o exemplo que estamos utilizando e evoluindo é a classe SaldoDaContaAlterado.

public interface IHandler<T> 
    where T : IDomainEvent
{
    void Handle(T @event);
}

Com a interface criada, temos que implementar a mesma em classes que serão consideradas os tratadores dos eventos, substituindo o tipo T por algum evento que nosso domínio define e estamos interessados em sermos notificados quando ele acontecer. Dentro da implementação do método Handle ficamos livres para executar tudo o que for necessário para aquele contexto, e que no nosso caso, é colocar “uma lupa” sobre o cliente que está com saldo negativo. Note que como parâmetro do método Handle temos (ou deveríamos ter) todas as informações necessárias a respeito do que ocorreu.

public class MonitorDeClientes : IHandler<SaldoDaContaAlterado>
{
    public void Handle(SaldoDaContaAlterado @event)
    {
        if (@event.SaldoAtual < @event.SaldoAnterior)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(
                "Monitorando o Cliente {0}. Saldo: {1:N2}",
                @event.NomeDoCliente,
                @event.SaldoAnterior);

            Console.ResetColor();
        }
    }
}

Uma vez que a classe concreta está criada e implementada, precisamos acopla-la a execução para que ela seja executada. Agora fazemos o uso da classe DomainEvents para acomodar a relação dos eventos de domínio. Aqui temos duas formas de proceder, sendo uma lista de tratadores estáticos ou de tratadores dinâmicos. Os tratadores estáticos permitem à aplicação já identificar todos os tratadores existentes, em outras palavras, podemos utilizar Reflection para encontrar todas as classes que implementam a interface IHandler<T> e adiciona-las a coleção de tratadores da aplicação, e também via Reflection, instanciarmos essas classes que representam os eventos toda vez em que ele for disparado pela domínio.

public static class DomainEvents
{
    private static List<Type> handlers = new List<Type>();

    static DomainEvents()
    {
        handlers =
            (
                from t in Assembly.GetExecutingAssembly().GetTypes()
                from i in t.GetInterfaces()
                where
                    i.IsGenericType &&
                    i.GetGenericTypeDefinition() == typeof(IHandler<>)
                select t
            ).ToList();
    }

    public static void Raise<T>(T @event) where T : IDomainEvent
    {
        handlers.ForEach(h =>
        {
            if (typeof(IHandler<T>).IsAssignableFrom(h))
                ((IHandler<T>)Activator.CreateInstance(h)).Handle(@event);
        });
    }
}

O uso externo da classe ContaCorrente não muda em nada, ou seja, continuamos interagindo com os métodos públicos que ela expõe. Como a varredura em busca por classes que implementam a interface IHandler<T> está no construtor estático da classe DomainEvents, tão logo quando a aplicação for inicializada os tipos serão identificados e adicionado, e quando o método Raise for invocado quando um lançamento de débito ou crédito ocorrer, o nome do cliente e seu saldo serão apresentados na tela em cor vermelha.

var cc = new ContaCorrente("Israel Aece");
cc.Lancar(new ContaCorrente.Lancamento("Pagto de Energia", -1000M));

A outra opção que temos é a relação dinâmica de tratadores, onde também podemos utilizar Reflection para descobrir os tratadores que implementam a interface IHandler<T>, porém há a possibilidade de dinamicamente adicionar novos tratadores em tempo de execução de acordo com a necessidade através do método Register. O método Raise agora já não instancia dinamicamente o tratador, ou seja, isso é responsabilidade do código que o consome, dando a possibilidade de fazer uso da instância antes e depois se desejar, o que pode ser útil durante os testes para saber se o evento foi o não disparado.

public class MonitorDeClientes : IHandler<SaldoDaContaAlterado>
{
    public readonly List<string> ClientesMonitorados = new List<string>();

    public void Handle(SaldoDaContaAlterado @event)
    {
        if (@event.SaldoAtual < @event.SaldoAnterior)
            this.ClientesMonitorados.Add(@event.NomeDoCliente);
    }
}

Neste modelo, para exemplificar, ao invés de escrever na tela o cliente monitorado, o adicionamos na coleção de clientes, que nada mais é que um campo da classe. E a classe DomainEvents também mudará a sua implementação para possibilitar o vínculo dinâmico de eventos, onde temos um dicionário que para cada tipo de evento uma coleção de delegates é criada.

public static class DomainEvents
{
    private static Dictionary<Type, List<Delegate>> handlers =
        new Dictionary<Type, List<Delegate>>();

    static DomainEvents()
    {
        handlers =
            (
                from t in Assembly.GetExecutingAssembly().GetTypes()
                where
                    !t.IsInterface &&
                    typeof(IDomainEvent).IsAssignableFrom(t)
                select t
            ).ToDictionary(t => t, t => new List<Delegate>());
    }

    public static void Register<T>(Action<T> handler) where T : IDomainEvent
    {
        handlers[typeof(T)].Add(handler);
    }

    public static void Raise<T>(T @event) where T : IDomainEvent
    {
        handlers[typeof(T)].ForEach(h => ((Action<T>)h)(@event));
    }
}

Por fim, o código que consome também sofrerá uma alteração para exibir o uso monitor antes e depois do evento que foi disparado.

var monitor = new MonitorDeClientes();
DomainEvents.Register<SaldoDaContaAlterado>(monitor.Handle);

var cc = new ContaCorrente("Israel Aece");
cc.Lancar(new ContaCorrente.Lancamento("Pagto de Energia", -1000));

Console.WriteLine("Qtde: {0}", monitor.ClientesMonitorados.Count);

Em ambas as técnicas é possível ter diversos tratadores para um mesmo evento gerado. Isso é comum e muito mais elegante do que em um simples tratador realizar mais tarefas do que ele deveria fazer. Se ele é responsável por monitorar, não deveria ser responsável por notificar o gerente que a conta de seu cliente ficou negativa. Nos tratadores também vale o princípio de responsabilidade única para garantir uma fácil manutenção e legibilidade.

Para finalizar, essas técnicas funcionam bem, mas existem alguns problemas funcionais que podem tornar o sistema propício a falhas. Mas isso será assunto do próximo artigo da série.

Eventos de Domínio – Geração

Os eventos de domínio nos permite identificar ações importantes que ocorrem em nossa aplicação e que desejamos divulga-la para os interessados. Por interessados, leia-se outras aplicações ou, principalmente, outros contextos que estão interligados e que reagem aos eventos para realizar uma outra atividade relacionada aquela que acabou de acontecer. Para um exemplo simples, considere uma conta corrente que ao atingir o valor negativo, a central de risco do banco deve ser acionada para entender o que está havendo com o cliente e, eventualmente, monitorar as suas atividades financeiras para evitar um prejuízo maior.

O lançamento de débito ou crédito se dá na conta corrente, e se a regra de monitoramento for atendida, temos que passar a monitorar o respectivo cliente. Incorporar eventos à classe correspondente, que neste caso é a classe que representa a conta corrente, ajudará em uma centralização de código, fácil manutenção e, principalmente, agregando à ela a responsabilidade de notificar que o saldo foi alterado (para cima ou para baixo). Competirá aos consumidores a usar a informação de acordo com a sua necessidade. O monitor de risco talvez não esteja interessado em uma conta que ficou “menos negativa”.

public class ContaCorrente
{
    //Outros membros ocultados

    public void Lancar(Lancamento lancamento)
    {
        var saldoAnterior = this.Saldo;

        this.lancamentos.Add(lancamento);
        this.Saldo += lancamento.Valor;
    }
}

Por agora, tudo o que o método acima faz é alteração da propriedade que armazena o saldo e inclui um novo lançamento na coleção interna. Depois do saldo alterado, chega o momento da conta corrente gerar o evento para notificar a alteração no saldo. A implementação padrão de eventos de domínio consiste na criação de uma marker interface, que geralmente não possui nenhum membro. Ao contrário do que acontece no .NET, onde os eventos são representados por delegates, no domínio utilizamos simples classes que implementam esta interface:

public interface IDomainEvent { }

A nomenclatura destas classes são sempre definidas no passado, que indicará que algo já ocorreu, por exemplo: NovoPedidoAdicionado, NotaFiscalEmitida, e para o nosso exemplo, SaldoDaContaAlterado. Vale lembrar que a nomenclatura deve expressar, e muito, exatamente o que ocorreu. E como já era de se esperar, todas as classes que representam eventos devem implementar a interface IDomainEvent que será útil para garantirmos a construção e uso dos tipos que envolvem a infraestrutura de eventos:

public class SaldoDaContaAlterado : IDomainEvent
{
    public SaldoDaContaAlterado(
        string nomeDoCliente, decimal saldoAnterior, decimal saldoAtual)
    {
        this.NomeDoCliente = nomeDoCliente;
        this.SaldoAnterior = saldoAnterior;
        this.SaldoAtual = saldoAtual;
    }

    public string NomeDoCliente { get; private set; }

    public decimal SaldoAnterior { get; private set; }

    public decimal SaldoAtual { get; private set; }
}

É importante notarmos que a classe que representa o evento possui algumas propriedades para descrever o que ocorreu, mas sempre temos que nos atentar em o que colocar ali, tentando manter a regra do mínimo possível necessário. Seguem algumas considerações importantes que devemos ter em mente durante a construção destas classes:

  • Entidades: é tentador colocar nestas propriedades a própria entidade que sofreu a alteração (ContaCorrente). Devemos ao máximo evitar isso, pois causará uma dependência destas classes para os interessados ao evento. Muitas vezes os eventos serão utilizados para comunicação entre contextos, e isso evitará a necessidade de referenciar a entidade (física e virtualmente). Tente optar sempre por tipos primitivos.
  • Imutabilidade: essas classes não devem ter qualquer funcionalidade (métodos), apenas os dados que correspondem ao evento gerado. As propriedades são de somente leitura e são abastecidas no construtor.
  • Ids: o Id da entidade pode ser definido no evento, mas isso só faz sentido se o consumidor já estiver colaborando com o gerador e tiver condições de recarregar a entidade, ou seja, ter acesso ao mesmo repositório. Caso o acesso não seja possível, teremos que incorporar na classe todas as informações necessárias para reportar a alteração, nem que seja necessário a duplicação de todas as informações da entidade.

Uma vez que os eventos já estão definidos, chega o momento de disparar. Como mencionado acima, o responsável pelo disparo será a própria classe que identifica a mudança, e que neste caso é a ContaCorrente, e sem qualquer análise de outra condição, dispara o evento informando que o saldo foi alterado. Note que ela irá recorrer à classe estática chamada de DomainEvents.

public class ContaCorrente
{
    //Outros membros ocultados

    public void Lancar(Lancamento lancamento)
    {
        var saldoAnterior = this.Saldo;

        this.lancamentos.Add(lancamento);
        this.Saldo += lancamento.Valor;

        DomainEvents.Raise(
            new SaldoDaContaAlterado(this.NomeDoCliente, saldoAnterior, this.Saldo));
    }
}

Agora é de responsabilidade da classe DomainEvents encontrar os consumidores deste tipo de evento e notifica-los da alteração. Há diversas técnicas que podemos utilizar na implementação da classe DomainEvents bem como em seus consumidores, mas que merece um artigo específico, e será abordado na sequência desta série.

Migrando de DAAB para Dapper

Um dos pilares do antigo Enterprise Library é o DAAB (Data Access Application Block), e como o próprio nome diz, é um componente que auxilia na extração de dados de uma base de dados relacional. Sua API fornece métodos que permitem executar consultas que retornam valore escalares e também materializar o resultado em objetos.

É importante salientar que ele não é um ORM, e sendo assim, não rastreia mudanças em objetos depois que eles são materializados; compete ao cliente que faz uso desta biblioteca gerenciar tais mudanças que ocorrem nos mesmos, para depois devolver as alterações para a base de dados executando comandos de UPDATE, DELETE ou INSERT.

Abaixo um exemplo simples de como materializar o resultado para um objeto. Além da consulta SQL, temos que ter um gerador de parâmetros que os cria e anexa ao comando e, por fim, um builder, que permite customizar o mapeamento entre o resultado e as propriedades expostas pela classe mencionada.

var cliente =
database.CreateSqlStringAccessor<Cliente>
(
@”
SELECT
 c.ClienteId As ‘Id’
, e.RazaoSocial
, e.Cnpj
FROM Cliente c
INNER JOIN Empresa e ON e.EmpresaId = c.EmpresaId
WHERE
c.ClienteId = @p0″,
new ParameterMapper(),
MapBuilder<Cliente>
.MapNoProperties()
.MapByName(c => c.Id)
.MapByName(c => c.RazaoSocial)
.MapByName(c => c.Cnpj)
.Build()
).Execute(clienteId).SingleOrDefault();

O método CreateSqlStringAccessor<TResult> possui uma sobrecarga que permite apenas passar a consulta, e o mapeamento é feito de forma automática se ele encontrar as propriedades com os mesmos nomes das colunas. Além destes facilitadores para converter o resultado em objetos conhecidos pela aplicação, o DAAB gerencia a conexão para nós, ou seja, ele abre o mais tarde e fecha o mais cedo possível.

Isso é válido, pois evita precisamos gerenciar manualmente a conexão, pois algum descuido pode deixá-la aberta consumindo recursos desnecessários ou esgotar a quantidade máxima de conexões que o banco de dados possui. Só que em alguns casos o que queremos é manter a conexão aberta para executar mais de uma consulta, pois o fechamento e a abertura logo em seguida pode ser mais oneroso do que mante-la aberta por um curto período de tempo. E quando isso é necessário, temos novamente que voltar a lidar com as classes já tão conhecidas para trabalhar com dados no .NET (Connection, Command, DataReader, Parameter, etc.).

Infelizmente já faz algum tempo que esta biblioteca não sofre atualização (2013). Como uma – boa – alternativa, um Micro-ORM chamado Dapper foi criado por Sam Saffron e Marc Gravell para ser usado pelo site StackOverflow (um grande case de performance) e hoje é open-source e pode ser utilizado por nossas aplicações. Ele foi criado com foco em performance, otimizando e reutilizando internamente objetos para tornar as consultas recorrentes cada vez mais veloz.

A sua API é extremamente simples, e faz uso de métodos de extensão na sobre a interface IDbConnection para materializar o resultado em objetos. Através de um método genérico chamado Query<T>, nós informamos a consulta SQL, e a parametrização é bem mais simples de se configurar, pois basta informar os parâmetros através de objetos anônimos, que o Dapper irá mapear as propriedades do objeto em parâmetros na consulta SQL.

using (var conn = new SqlConnection(“Data Source=.;Initial Catalog=Dados;Integrated Security=True”))

{
    var cliente = conn.Query<Cliente>

(@”
SELECT
              c.ClienteId As ‘Id’
    , e.RazaoSocial
    , e.Cnpj
FROM Cliente c
INNER JOIN Empresa e ON e.EmpresaId = c.EmpresaId
WHERE
    c.ClienteId = @id”, new { id = 2688 }
).SingleOrDefault();

    Console.WriteLine(cliente.RazaoSocial);
}

Aqui, como podemos notar, somos nós que temos que gerenciar a conexão, mas ao contrário do que ocorre com o DAAB, a forma de se escrever é bem mais simples. E como é possível notar no próprio site do projeto, o uso desta biblioteca é extremamente eficiente.

Composição de Tokens para Cancelamento

Ao contrário do que temos na classe Thread, a classe Task não fornece um método para abortar a execução da mesma. Se desejamos “monitorar” a execução e ter a chance de cancelar, temos que passar uma espécie de token ao criar a tarefa, e externamente, quando quisermos, podemos cancelar a execução. Compete aquele que programa a tarefa a ser executada, avaliar se o cancelamento foi ou não solicitado.

No .NET Framework a classe que nos permite controlar o cancelamento é a CancellationTokenSource. Ela expõe uma propriedade chamada Token que por sua vez, é representada pela classe CancellationToken, e é este valor que temos que passar para a tarefa a ser executada, e através do método Cancel podemos solicitar o cancelamento. Um detalhe importante aqui é que a classe CancellationTokenSource também disponibiliza um método estático chamado CreateLinkedTokenSource, que nos permite agrupar vários tokens, e quando qualquer um deles for cancelado, a tarefa é cancelada.

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

using (var cts3 = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token))
{
    var task = Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < int.MaxValue; i++)
        {
            cts3.Token.ThrowIfCancellationRequested();
            Console.WriteLine(i);
        }
    }, cts3.Token);
               
    Console.ReadLine();
    cts2.Cancel();
    Console.ReadLine();
}

Repare que criamos dois tokens (cts1 e cts2) e agrupamos em um terceiro chamado cts3. Se invocarmos o método Cancel de qualquer um dos três, a tarefa será interrompida. Vale lembrar que somente o fato de passar o token na criação da tarefa não é suficiente para interromper a execução; como foi dito acima, é de responsabilidade do desenvolver monitorar a solicitação de cancelamento, e para isso, neste caso estamos utilizando o método ThrowIfCancellationRequested, que dispara uma exceção se a solicitação foi feita.

Por fim, note que apenas o terceiro CancellationTokenSource está sendo envolvido em um bloco using. Isso é porque quando criamos este objeto a partir do link entre outros, o método Dispose irá executar algumas atividades que irão impactar a memória, descartando objetos que são criados exclusivamente para isso. O método Dispose nesta classe também pode ser útil quando estamos utilizando a funcionalidade CancelAfter, que utiliza internamente um Timer para monitorar o tempo e, automaticamente, interromper a tarefa que está – ainda – sendo executada depois que o tempo expira.