C#

Testes unitários no C# com MSTest

Continuando a minha série de artigos sobre as principais bibliotecas de teste do C#, neste artigo abordarei a biblioteca criada e mantida pela Microsoft, a MSTest. Caso queria conhecer a NUnit ou a XUnit, não deixe de ver os artigos anteriores.

Conhecendo a biblioteca MSTest

O Microsoft Test Framework, mais conhecido como MSTest, nasceu como uma ferramenta de testes unitários para aplicações .NET integrada com o Visual Studio. Hoje ela é uma biblioteca separada, que possui recursos poderosos, porém simples e de fácil compreensão.

C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO

Criando o projeto que será testado

Assim como nos demais artigos, iremos criar uma solução:

dotnet new sln -n teste-unitario-com-mstest

Criar um projeto de biblioteca de classes:

dotnet new classlib -n Calculos

E criar uma classe, que será testada:

public class Calculadora
{
    public int Soma(int operador1, int operador2) => operador1 + operador2;
    public int Subtracao(int operador1, int operador2) => operador1 - operador2;
    public int Multiplicao(int operador1, int operador2) => operador1 * operador2;
    public int Divisao(int dividendo, int divisor) => dividendo / divisor;
    public (int quociente, int resto) RestoDivisao(int dividendo, int divisor) => (dividendo / divisor, dividendo % divisor);
}

Por fim, é necessário adicionar a referência deste projeto na solução:

dotnet sln teste-unitario-com-mstest.sln add Calculos/Calculos.csproj

Criando o projeto de teste

Sabemos que um projeto de teste se trata de um projeto de biblioteca de classes que contém a referência de uma biblioteca de teste. Felizmente, o .NET fornece um template que já cria um projeto com a referência da biblioteca de teste. Para o MSTest o projeto pode ser criado com o comando abaixo:

dotnet new mstest -n Calculos.Tests

Este projeto será adicionado na solução:

dotnet sln teste-unitario-com-mstest.sln add Calculos.Tests/Calculos.Tests.csproj

E o projeto Calculos será referenciado nele:

dotnet add Calculos.Tests/Calculos.Tests.csproj reference Calculos/Calculos.csproj

Agora podemos conhecer os recursos desta biblioteca.

Adicionando os testes unitários

No projeto de teste, Calculos.Tests exclua o arquivo TestUnit1.cs e adicione um novo arquivo chamado CalculadoraTests.cs, neste arquivo iremos criar uma classe com o mesmo nome, que terá a estrutura abaixo:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Calculos.Tests
{
    [TestClass]
    public class CalculadoraTests
    {
        [TestMethod]
        public void Soma_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Soma(10, 20);
            //Verifica se o resultado é igual a 30
            Assert.AreEqual(30, resultado);
        }
    }
}

Observe que é utilizado o atributo TestClass para indicar que a classe criada é uma classe de teste. Dentro dela, há um método marcado com o atributo TestMethod, que sinaliza que este é um método de teste.

Quando o runner da biblioteca for executado, ele irá analisar dentro do projeto quais são as classes que possuem o atributo TestClass e irá chamar os métodos marcados com o atributo TestMethod.

Diferente de outras bibliotecas, o runner da MSTest não é executado diretamente, ele é chamado quando o teste é executado utilizando o comando dotnet test ou Test Explorer do Visual Studio.

Para este artigo, utilizarei a linha de comando:

Note que o único teste definido passou.

Definindo vários testes unitários de uma só vez

No momento a nossa classe de teste possui apenas um teste unitário, que analisa um grupo específico de dados de entrada. Caso seja necessário analisar vários dados de entrada, pelo que vimos até o momento, seria necessário criar vários testes.

Felizmente o MSTest possui os atributos DataTestMethod e DataRow que permite criar um método de teste, que varia apenas os dados de entrada:

[DataTestMethod]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(4)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.AreEqual(0, resultado.resto);
}

Ao executar os testes:

Note que cada dado de entrada informado nos DataRow é considerado um teste a parte e o grupo é considerado outro teste. É por isso que a contagem de testes é 6.

Se o teste não passar com um dos valores:

[DataTestMethod]
[DataRow(1)]
[DataRow(2)]
[DataRow(3)]
[DataRow(5)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.AreEqual(0, resultado.resto);
}

Teremos o resultado:

Ou seja, um dos testes não passou, assim é indicado que o grupo também não passou. Por isso que é indicado dois erros.

Para finalizar, e obter uma cobertura de 100% dos testes, vamos definir método de testes para os outros métodos da nossa classe de Calculadora:

[TestClass]
public class CalculadoraTests
{
    [TestMethod]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30
        Assert.AreEqual(30, resultado);
    }

    [DataTestMethod]
    [DataRow(1)]
    [DataRow(2)]
    [DataRow(3)]
    [DataRow(4)]
    public void RestoDivisao_DeveRetornarZero(int value)
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(12, value);
        //Verifica se o resto da divisão é 0
        Assert.AreEqual(0, resultado.resto);
    }

    [TestMethod]
    public void RestoDivisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(10, 3);
        //Verifica se o quociente da divisão é 3 e o resto 1
        Assert.AreEqual(3, resultado.quociente);
        Assert.AreEqual(1, resultado.resto);
    }

    [TestMethod]
    public void Subtracao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Subtracao(20, 10);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }

    [TestMethod]
    public void Divisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Divisao(100, 10);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }

    [TestMethod]
    public void Multiplicao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Multiplicao(5, 2);
        //Verifica se o resultado é igual a 10
        Assert.AreEqual(10, resultado);
    }
}
C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

Demais comparadores do MSTest

Nos exemplos apresentados neste artigo, utilizamos somente o comparador AreEqual. Felizmente a biblioteca fornece uma série de comparadores que podem ser visualizados na documentação dela.

Sendo uma das bibliotecas de testes unitários mais simples do C#, com ela não há desculpa para não adicionar testes unitários nas suas aplicações.

Fico por aqui, até o próximo artigo.

Testes unitários no C# com o xUnit

No meu último artigo comentei sobre o hábito de sempre adicionar testes unitários as aplicações que desenvolvo e sobre o meu desejo de criar uma série de artigos abordando as bibliotecas de testes unitários do C#. Então dando continuidade a este tema, neste artigo abordarei a biblioteca xUnit.

Conhecendo a biblioteca xUnit

O xUnit é uma biblioteca open source de testes unitários, que foi criada pelo mesmo criador da segunda versão do NUnit, James Newkirk. Desta forma, ambas as bibliotecas possuem funcionalidades similares.

Criando o projeto que será testado

Como no artigo anterior, para exemplificar a biblioteca xUnit, iremos criar uma solução:

dotnet new sln -n teste-unitario-com-xunit

E nela adicionar uma biblioteca de classes:

dotnet new classlib -n calculos

Que conterá uma classe Calculadora:

public class Calculadora
{
    public int Soma(int operador1, int operador2) => operador1 + operador2;
    public int Subtracao(int operador1, int operador2) => operador1 - operador2;
    public int Multiplicao(int operador1, int operador2) => operador1 * operador2;
    public int Divisao(int dividento, int divisor) => dividento / divisor;
    public (int quociente, int resto) RestoDivisao(int dividento, int divisor) => (dividento / divisor, dividento % divisor);
}

Adicione a referência deste projeto na solução:

dotnet sln teste-unitario-com-xunit.sln add calculos/calculos.csproj

Criando o projeto de teste

Um projeto de teste trata-se de uma biblioteca de classe que contenha a referência xunit. Felizmente no .NET Core, há a opção de um template de projeto já com esta biblioteca adicionada. Para criar este tipo de projeto utilize o comando abaixo:

dotnet new xunit -n calculos.tests

Adicione este projeto na solução:

dotnet sln teste-unitario-com-xunit.sln add calculos.tests/calculos.tests.csproj

E adicione nele a referência da biblioteca de cálculos nele:

dotnet add calculos.tests/calculos.tests.csproj reference calculos/calculos.csproj

Pronto, agora podemos adicionar testes unitários na nossa aplicação.

Adicionando testes unitários

No projeto de teste criado, remova o arquivo UnitTest1.cs e adicione uma classe chamada CalculadoraTest, nela adicione o método abaixo:

public class CalculadoraTest
{
    [Fact]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30        
        Assert.AreEqual(30, resultado);
    }
}

Note que no código acima é utilizado o atributo em cima do método Soma_DeveRetornarOValorCorreto. Isso indica que este é um método de teste. É a graças a ele que os tests runner sabem qual método deve ser chamado quando um teste é iniciado.

Este teste pode ser executado através do comando dotnet test, o Test Explorer do Visual Studio ou o xUnit Runner Console.

Com o teste definido, ele pode ser executado:

dotnet test

Onde teremos resultado:

Note que ele indica que o teste definido passou.

C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO

Também é possível criar uma “teoria”. Teoria executa o mesmo teste com uma série de parâmetros. Caso algum dos parâmetros gere um resultado inesperado, ela é considerada falha.

[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(5)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.Equal(0, resultado.resto);
}

Repare que a teoria é definida com a anotação Theory e que cada parâmetro testado é indicado pela anotação InlineData.

Caso o teste falhe, é indicado com qual valor isso ocorreu:

Para finalizar e para obtermos uma cobertura dos testes de 100%, vamos definir testes para os demais métodos de Calculadora:

public class CalculadoraTest
{
    [Fact]
    public void Soma_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Soma(10, 20);
        //Verifica se o resultado é igual a 30        
        Assert.Equal(30, resultado);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    public void RestoDivisao_DeveRetornarZero(int value)
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(12, value);
        //Verifica se o resto da divisão é 0
        Assert.Equal(0, resultado.resto);
    }

    [Fact]
    public void RestoDivisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.RestoDivisao(10, 3);
        //Verifica se o quociente da divisão é 3 e o resto 1
        Assert.Equal(3, resultado.quociente);
        Assert.Equal(1, resultado.resto);
    }

    [Fact]
    public void Subtracao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Subtracao(20, 10);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }

    [Fact]
    public void Divisao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Divisao(100, 10);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }

    [Fact]
    public void Multiplicao_DeveRetornarOValorCorreto()
    {
        Calculadora c = new Calculadora();
        var resultado = c.Multiplicao(5, 2);
        //Verifica se o resultado é igual a 10
        Assert.Equal(10, resultado);
    }
}

Demais comparadores do xUnit

O exemplo mostrado aqui é simples, nele fazemos uso apenas do comparador Assert.Equal. Para o caso do exemplo apenas este comparador foi necessário. Agora sempre que um método da classe for alterado, os testes podem ser executados para verificar se a alteração gerou algum impacto nos demais métodos dela.

Infelizmente a documentação do xUnit não explica os comparadores que podem ser utilizados com a biblioteca. Eles só são listados na comparação com outras bibliotecas de teste.

Caso seja adicionada alguma referência para eles na documentação, a adicionarei aqui posteriormente.

Por hoje é isso. No meu próximo artigo falarei do MSTest, até lá!

Teste unitários no C# com o NUnit

Um hábito que estou procurando cultivar nos últimos tempos é sempre adicionar testes unitários nas minhas aplicações. Qualquer desenvolvedor sabe (ou deveria saber) os benefícios de se adicionar testes unitários em uma aplicação, mas infelizmente não são todos que seguem esta filosofia. Para ajudar nisso, criarei uma série de artigos falando sobre as bibliotecas de testes unitários do C#, começando pela NUnit.

Conhecendo a biblioteca NUnit

O NUnit é uma biblioteca open source de teste unitários, que nasceu como um porte para o .NET da biblioteca de teste unitários do Java, a JUnit. Com o tempo ela foi sendo reescrita em C#. Agora, na versão atual, a 3, a biblioteca foi totalmente reescrita para que fosse adicionado novos recursos e fizesse uso de recursos fornecidos pela plataforma .NET.

Devido a sua gama de recursos e facilidade de uso, no momento, é a biblioteca de testes unitários mais popular do .NET.

Para conhecê-la, vamos colocar a mão na massa.

Criando o projeto que será testado

Diferente de outras bibliotecas, os testes unitários devem ser adicionados em um projeto a parte. O .NET fornece templates de projetos para algumas bibliotecas de testes, incluindo o NUnit, mas caso seja necessário definir isso manualmente, basta criar um projeto de biblioteca de classes.

Por necessitar de dois projetos, vamos iniciar a criação da aplicação utilizada neste artigo com a criação da solução:

dotnet new sln -n teste-unitario-com-nunit

Dentro da pasta da solução, crie uma biblioteca de classes:

dotnet new classlib -n calculos

No projeto criado, renomeie o arquivo Class1 para Calculadora e nela adicione os métodos abaixo:

public class Calculadora
{
    public int Soma(int operador1, int operador2) => operador1 + operador2;
    public int Subtracao(int operador1, int operador2) => operador1 - operador2;
    public int Multiplicacao(int operador1, int operador2) => operador1 * operador2;
    public int Divisao(int dividendo, int divisor) => dividendo / divisor;
    public (int quociente, int resto) RestoDivisao(int dividendo, int divisor) => (dividendo / divisor, dividendo % divisor);
}
C# (C Sharp) - TDD
Curso de C# (C Sharp) - TDD
CONHEÇA O CURSO

Por fim, adicione o projeto na solução:

dotnet sln teste-unitario-com-nunit.sln add calculos/calculos.csproj

Criando o projeto de teste

Como disse, o .NET fornece alguns templates de projetos de teste para algumas bibliotecas. Para a NUnit, o projeto pode ser criado com o comando abaixo:

dotnet new nunit -n calculos.tests

Adicione este projeto na solução:

dotnet sln teste-unitario-com-nunit.sln add calculos.tests/calculos.tests.csproj

E adicione ao projeto a referência da nossa biblioteca de classes, o projeto calculos:

dotnet add calculos.tests/calculos.tests.csproj reference calculos/calculos.csproj

Agora renomeie o arquivo UnitTest1.cs para CalculadoraTest.cs e altere o código do arquivo para:

using NUnit.Framework;
using calculos;

namespace calculos.tests
{
    [TestFixture]
    public class CalculadoraTest
    {
        [Test]
        public void Soma_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Soma(10, 20);
            //Verifica se o resultado é igual a 30
            Assert.AreEqual(30, resultado);
        }
    }
}

No código acima, o atributo [TestFixture] indica que a classe contém testes unitários. Já o atributo [Test] indica que o método é um método de teste. É através desses atributos que o comando dotnet test e o Test Explorer do Visual Studio conseguem localizar os testes unitários do projeto.

Dentro do nosso método de teste, é utilizado a classe estática Assert para ter acesso ao método AreEqual, que verifica se o primeiro parâmetro é igual ao segundo.

Na versão 3 da biblioteca foi introduzido a sintaxe “constraint”, onde o teste acima pode ser escrito da seguinte forma:

[Test]
public void Soma_DeveRetornarOValorCorreto()
{
    Calculadora c = new Calculadora();
    var resultado = c.Soma(10, 20);
    //Verifica se o resultado é igual a 30
    Assert.That(30, Is.EqualTo(resultado));
}

Como a biblioteca recomenda o uso desta sintaxe, todos os testes mostrados neste artigo implementarão ela.

Com o teste definido, podemos executá-lo com o comando abaixo:

dotnet test

O resultado será:

O nosso único teste passou.

Um outro recurso adicionado na última versão da biblioteca é a possibilidade de agrupar os comparadores:

[Test]
public void Divisao_DeveRetornarOValorCorreto()
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(10, 3);
    //Verifica se o quociente da divisão é 3 e o resto 1
    Assert.Multiple(() =>
    {
        Assert.That(3, Is.EqualTo(resultado.quociente));
        Assert.That(1, Is.EqualTo(resultado.resto));
    });
}

A vantagem de se agrupar comparadores é que caso qualquer um falhe, o teste não é finalizado. A falha é salva e os demais testes são executados. Ao final, o resultado de todos é agrupado. O teste só será finalizado caso seja gerada alguma exceção não tratada. Além disso, o bloco aceita outros códigos além de asserts, mas não é permitido o uso os asserts abaixo:

  • Assert.Pass;
  • Assert.Ignore;
  • Assert.Inconclusive;
  • Assume.That.

Definindo vários testes unitários de uma só vez

No momento definimos apenas dois testes. Seguindo a lógica mostrada, para cada teste unitário é necessário definir um método de teste. Felizmente, o NUnit possui o atributo TestCase, que nos permite definir mais de um teste, variando apenas os argumentos:

[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void RestoDivisao_DeveRetornarZero(int value)
{
    Calculadora c = new Calculadora();
    var resultado = c.RestoDivisao(12, value);
    //Verifica se o resto da divisão é 0
    Assert.That(0, Is.EqualTo(resultado.resto));
}

Desta forma, para o NUnit estão sendo definidos três métodos de teste, por isso que ao executá-los é mostrado cinco testes:

Por fim, para que a cobertura dos testes seja 100%, vamos definir método de testes para os outros métodos da nossa classe de Calculadora:

using NUnit.Framework;
using calculos;

namespace calculos.tests
{
    [TestFixture]
    public class CalculadoraTest
    {
        [Test]
        public void Soma_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Soma(10, 20);
            //Verifica se o resultado é igual a 30
            Assert.That(30, Is.EqualTo(resultado));
        }

        [Test]
        public void RestoDivisao_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.RestoDivisao(10, 3);
            //Verifica se o quociente da divisão é 3 e o resto 1
            Assert.Multiple(() =>
            {
                Assert.That(3, Is.EqualTo(resultado.quociente));
                Assert.That(1, Is.EqualTo(resultado.resto));
            });
        }

        [TestCase(1)]
        [TestCase(2)]
        [TestCase(3)]
        public void RestoDivisao_DeveRetornarZero(int value)
        {
            Calculadora c = new Calculadora();
            var resultado = c.RestoDivisao(12, value);
            //Verifica se o resto da divisão é 0
            Assert.That(0, Is.EqualTo(resultado.resto));
        }

        [Test]
        public void Subtracao_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Subtracao(20, 10);
            //Verifica se o resultado é igual a 10
            Assert.That(10, Is.EqualTo(resultado));
        }

        [Test]
        public void Divisao_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Divisao(100, 10);
            //Verifica se o resultado é igual a 10
            Assert.That(10, Is.EqualTo(resultado));
        }

        [Test]
        public void Multiplicacao_DeveRetornarOValorCorreto()
        {
            Calculadora c = new Calculadora();
            var resultado = c.Multiplicacao(5, 2);
            //Verifica se o resultado é igual a 10
            Assert.That(10, Is.EqualTo(resultado));
        }
    }
}
Teste de Software Básico
Curso de Teste de Software Básico
CONHEÇA O CURSO

Demais comparadores do NUnit

O exemplo mostrado aqui é simples, mas ele demonstra a importância da definição de testes em uma aplicação. Agora sempre que os métodos da classe Calculadora forem alterados, os testes podem ser executados e verificados se algum dos comportamentos existentes foi impactado com a alteração.

Neste exemplo é utilizado apenas o comparador de igualdade, que é um dos mais comuns. Você pode ver os demais comparadores disponíveis na documentação do NUnit.

No meu próximo artigo abordarei o XUnit, até lá 🙂

Switch Expressions no C# 8.0

Na versão 7.0 do C# foi introduzido o conceito de Pattern Matching, que tem o intuito de evitar a necessidade da implementação de typecast. Já na versão 8.0, este conceito foi aplicado, sendo aplicado ao condicional switch, com a introdução das switch expressions.

Switch clássico

No switch clássico, utilizamos cases para comparar uma variável/objeto em relação a uma série de valores:

public Formato SelecionarFormato(object item)
{
    var formato = item as FormatoPlanta?;
    if (formato == null) return null;

    Formato formatoSelecionado = null;
    switch (formato)
    {
        case FormatoPlanta.Quadrado:
            formatoSelecionado = new Quadrado(Largura, Altura);
            break;
        case FormatoPlanta.Retangulo:
            formatoSelecionado = new Retangulo(Largura, Altura);
            break;
        case FormatoPlanta.Triangulo:
            formatoSelecionado = new Triangulo(Largura, Altura, 2);
            break;
    }
    return formatoSelecionado;
}

Implementando Pattern Matching no código

Com o pattern matching a conversão realizada no início do método acima pode ser substituída por um if:

public Formato SelecionarFormato(object item)
{
    if(item is FormatoPlanta formato)
    {
        Formato formatoSelecionado = null;
        switch (formato)
        {
            case FormatoPlanta.Quadrado:
                formatoSelecionado = new Quadrado(Largura, Altura);
                break;
            case FormatoPlanta.Retangulo:
                formatoSelecionado = new Retangulo(Largura, Altura);
                break;
            case FormatoPlanta.Triangulo:
                formatoSelecionado = new Triangulo(Largura, Altura, 2);
                break;
        }
        return formatoSelecionado;
    }
    else
        return null;
}
Desenvolvedor C# Pleno
Formação: Desenvolvedor C# Pleno
A formação Desenvolvedor C# nível Pleno da TreinaWeb tem um enfoque sobre a conectividade entre o .NET Framework e os bancos de dados relacionais através do ADO.NET. Também serão abordados os recursos para desenvolvedores que o Oracle e o MySQL oferecem, como functions, stored procedures e triggers.
CONHEÇA A FORMAÇÃO

Ela também pode ser aplicada diretamente no switch:

public Formato SelecionarFormato(object item)
{
    switch (item)
    {
        case FormatoPlanta formato when formato is FormatoPlanta.Quadrado:
            return new Quadrado(Largura, Altura);
        case FormatoPlanta formato when formato is FormatoPlanta.Retangulo:
            return new Retangulo(Largura, Altura);
        case FormatoPlanta formato when formato is FormatoPlanta.Triangulo:
            return new Triangulo(Largura, Altura, 2);
        default:
            return null;
    }
}

Agora que conhecemos as opções disponíveis atualmente, vamos conhecer a switch expression.

Utilizando a switch expression

Antes de mais nada, para fazer uso das switch expressions, a aplicação precisa ser configurada para o C# 8.0:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

</Project>

Com isso, o código pode ser refatorado novamente para a switch expression:

public Formato SelecionarFormato(object item) 
{
    return item switch
    {
        FormatoPlanta.Quadrado => new Quadrado(Largura, Altura),
        FormatoPlanta.Retangulo => new Retangulo(Largura, Altura),
        FormatoPlanta.Triangulo => new Triangulo(Largura, Altura, 2),
        _ => null
    };
}

Note que a variável/objeto a ser analisado vem antes da cláusula switch. Foi dispensado o uso do case, é informado apenas os valores a serem testados. No lugar de dois pontos, utiliza-se o =>. Para o valor padrão (default), é utilizado o underline.

Assim como no switch padrão, as opções são analisadas na ordem que forem informadas, então a opção padrão sempre deve ser a última da expressão.

Da mesma forma que o operador ternário, o resultado da switch expression precisa ser atribuída a uma variável (ou ser retornada como no exemplo acima).

Com isso, esta expressão só pode ser aplicada em cláusulas switch onde há retorno de dados. Caso nada for retornado, como no exemplo abaixo:

foreach (var item in formas)
{
    switch(item){
        case Triangulo t:
            t.Perimetro();
            break;
        case Retangulo r:
            r.Area();
            break;
        case 10:
            Console.WriteLine("Item é 10");
            break;
        case null:
            Console.WriteLine("Item é nulo");
            break;
        case Retangulo r when r.Largura > 0:
            r.Area();
            break;
        case var i:
            Console.WriteLine("Item é do tipo {0}", i?.GetType().Name);
            break;
    }
}

Este recurso não pode ser aplicado.

Além disso, após as setas da switch expression (=>) só é aceita uma expressão. Assim, caso queira processar um bloco de código, você pode contornar esta limitação com lambda:

public Formato SelecionarFormato(object item) 
{
    return item switch
    {
        FormatoPlanta.Quadrado => ((Func<Formato>)(() => {
                                    Console.WriteLine("Quatrado");
                                    return new Quadrado(Largura, Altura);
                                    }))(),
        FormatoPlanta.Retangulo => new Retangulo(Largura, Altura),
        FormatoPlanta.Triangulo => new Triangulo(Largura, Altura, 2),
        _ => null
    };
}

Por fim, no exemplo apresentado aqui não ocorre as situações, mas caso a cláusula switch esteja comparando uma propriedade do objeto:

string Display(object o)
{
    switch (o)
    {
        case Ponto p when p.X == 0 && p.Y == 0:
            return "origem";
        //...
    }
}

Pode ser aplicado o property pattern:

string Display(object o)
{
    return o switch
    {
        Ponto { X: 0, Y: 0 } => "origem",
        Ponto { X: var x, Y: var y } => $"({x}, {y})",
        {} => o.ToString(), // `o` não é nulo, mas não é do tipo Ponto
        null => "Nulo"
    };
}

Ou o desconstrutor:

string Display(int x, int y)
   => (x, y) switch {
        (0, 0) => "origem",
        //...
   };

Conclusão

Como é possível notar as switch expressions reduz o código e melhora a legibilidade (isso comparado com a cláusula padrão). Então caso utilize a versão 8.0 do C# não deixe de fazer uso deste ótimo recurso nas situações onde for possível.

Criando um middleware customizado para ASP.NET Core

Em um artigo anterior expliquei o pipeline do ASP.NET Core e o funcionamento dos middlewares neste tipo de aplicação. Nele apenas citei a possibilidade de criar middleware customizados. Como nesta semana necessitei realizar este procedimento, vou explicar neste artigo como isso pode ser feito.

Definindo o middleware customizado diretamente no pipeline

A forma mais simples de definir um middleware customizado é o adicionando diretamente no pipeline no método Configure da classe Startup:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Chamou nosso middleware (antes)");
        await next();
        await context.Response.WriteAsync("Chamou nosso middleware (depois)");
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Olá Mundo!");
    });
}

Como expliquei antes, os códigos executados no pipeline são middlewares, então se adicionarmos instruções, mesmo que seja indicando o que está havendo, como a acima, elas serão consideradas um middleware.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Definindo uma classe “Middleware”

No dia a dia você irá definir middlewares mais complexos, então o ideal é defini-los em uma classe a parte:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync("Chamou nosso middleware (antes)");
        await _next(httpContext);
        await httpContext.Response.WriteAsync("Chamou nosso middleware (depois)");
    }
}

A classe “Middleware” obrigatoriamente precisa ter a estrutura acima. Receber no construtor um objeto RequestDelegate e definir um método Invoke que recebe por parâmetro um objeto HttpContext.

Esta classe pode ser definida no pipeline utilizando o método UseMiddleware:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMiddleware<MyMiddleware>();
}

Mas o recomendado é definir um método de extensão para a interface IApplicationBuilder:

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

E utilizá-lo no Configure:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMyMiddleware();

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Olá Mundo!");
    });
}

Este é o padrão das bibliotecas que definem middlewares.

Definindo configurações para o Middleware

Existem algumas formas de definir configurações do middleware. Uma forma muito utilizada é especificá-las no método ConfigureServices. Para isso, é necessário definir uma classe com os atributos que o middleware receberá:

public class MyMiddlewareOptions 
{
    public string BeforeMessage { get; set; } = "Chamou nosso middleware (antes)";
    public string AfterMessage { get; set; } = "Chamou nosso middleware (depois)";
}

É recomendado que se defina valores padrão para as propriedades. Assim, se novas configurações não forem definidas, os valores padrão serão utilizados.

Essas opções podem ser passadas para o middleware como uma instância da classe, ou através de uma Action<T>, que é a forma mais utilizada. Para definir isso, é necessário adicionar um método de extensão para a interface IServiceCollection:

public static class MyMiddlewareExtensions
{
    public static IServiceCollection AddMyMiddleware(this IServiceCollection service, Action<MyMiddlewareOptions> options = default)
    {
        options = options ?? (opts => { });

        service.Configure(options);
        return service;
    }

    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Isso adicionará as opções no sistema de injeção de dependências do ASP.NET, o que nos permite obtê-las no middleware através de um parâmetro do construtor da classe:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly MyMiddlewareOptions _options;

    public MyMiddleware(RequestDelegate next, IOptions<MyMiddlewareOptions> options)
    {
        _next = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync(_options.BeforeMessage);
        await _next(httpContext);
        await httpContext.Response.WriteAsync(_options.AfterMessage);
    }
}

Agora o método AddMyMiddleware pode ser chamado no ConfigureServices e novas configurações podem ser indicadas:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMyMiddleware(options => {
        options.BeforeMessage = "Mensagem antes definida no ConfigureServices";
        options.AfterMessage = "Mensagem depois definida no ConfigureServices";
    });
}

Definindo as opções em uma classe, também é possível definir as configurações no arquivo appsettings.json:

{
  "MyMiddlewareOptionsSection": {
    "BeforeMessage": "Mensagem antes definida no appsettings",
    "AfterMessage": "Mensagem depois definida no appsettings"
  }
}

E indicá-las ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyMiddlewareOptions>(Configuration.GetSection("MyMiddlewareOptionsSection"));
}

Como o nome da sessão será informado, no arquivo appsettings.json, você poderia definir qualquer nome de sessão.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Com este tipo de configuração, mesmo se o middleware for adicionado mais de uma vez no pipeline, todas as versões utilizarão as mesmas configurações. Para especificar configurações para cada vez que ele for adicionado no pipeline, é necessário definir um método UseX que aceite essas configurações:

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }

    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder, Action<MyMiddlewareOptions> options = default)
    {
        var config = new MyMiddlewareOptions();
        options?.Invoke(config);
        return builder.UseMiddleware<MyMiddleware>(config);
    }
}

E modificar o construtor do middleware:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly MyMiddlewareOptions _options;

    public MyMiddleware(RequestDelegate next, MyMiddlewareOptions options)
    {
        _next = next;
        _options = options;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        await httpContext.Response.WriteAsync(_options.BeforeMessage);
        await _next(httpContext);
        await httpContext.Response.WriteAsync(_options.AfterMessage);
    }
}

Note que no lugar de aceitar um objeto de IOptions<T>, agora é aceito um objeto de MyMiddlewareOptions. Assim, o middleware pode ser adicionado duas vezes ao pipeline e receber configurações diferentes:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMyMiddleware(options => {
        options.BeforeMessage = "Primeira mensagem antes definida no Configure";
        options.AfterMessage = "Primeira mensagem depois definida no Configure";
    });

    app.UseMyMiddleware(options => {
        options.BeforeMessage = "Segunda mensagem antes definida no Configure";
        options.AfterMessage = "Segunda mensagem depois definida no Configure";
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

Caso queira utilizar os dois métodos, basta definir dois construtores no middleware, um aceitando objeto de IOptions<T> e outro aceitando objeto da sua classe de opções.

Conclusão

Neste exemplo exemplifiquei com um middleware simples as várias configurações disponíveis na criação de um middleware customizado. Caso trabalhe com aplicações ASP.NET, sempre que possível defina middleware customizados para manter um código limpo e reutilizar recursos.

Implementações padrão em interfaces no C# 8

A definição de interfaces na orientação a objetos as definem como “padrões definidos através de contratos que são formados com as classes“. Assim como contratos as interfaces definem cláusulas (assinaturas de métodos) que a pessoa que o assina (as classes que as implementam) deverão seguir.

Desta forma, sempre ficou muito claro que as classes são obrigadas a implementar todos os métodos declarados em uma interface. Devido a este comportamento, quando é necessário adicionar um novo método em uma interface, as opções sempre foram: também alterar todas as classes que a implementa ou criar uma nova interface.

Para mudar este cenário, no C# 8 foi adicionado um novo recurso: implementação padrão.

Desenvolvedor ASP.NET Full-Stack
Formação: Desenvolvedor ASP.NET Full-Stack
A formação Desenvolvedor ASP.NET Full Stack da TreinaWeb tem como objetivo abordar as duas principais plataformas dentro do ASP.NET: o ASP.NET MVC, para criação de aplicações web seguindo o padrão MVC/MVW; e o ASP.NET WebAPI, para criação de APIs RESTful que sigam os padrões mais atuais da indústria.
CONHEÇA A FORMAÇÃO

Implementação padrão

Agora é possível adicionar um corpo para métodos definidos em uma interface. Com isso, caso o método não seja declarado na classe ou estrutura que implementar a interface, não é gerado nenhum erro.

Vamos a um exemplo, suponha a interface abaixo:

public interface IRepositorio<T> 
{ 
    List<T> ObterTodos();
} 

Que é implementada pela classe:

public class PessoaRepositorio: IRepositorio<Pessoa>
{
    public List<Pessoa> ObterTodos()
    {
        //....
    }
}

Suponha que seja necessário alterar a interface, para adicionar um novo método. Agora é possível fazer isso sem quebrar nenhum código:

public interface IRepositorio<T>  
{ 
    List<T> ObterTodos();
    T PrimeiroRegistro() => ObterTodos().FirstOrDefault();
} 

Mesmo que a classe PessoaRepositorio não implemente este novo método, ela ainda está satisfazendo o contrato. Um objeto dela não terá acesso a ele, mas ele pode ser “convertido” para a interface, que terá acesso ao método:

public static void Primeiro(PessoaRepositorio repositorio)
{
    IRepositorio irepositorio = repositorio; // Convertendo para a interface
    var pessoa = irepositorio.PrimeiroRegistro(); //Chamando o novo método 
}

Claro que se a classe implementar o método, a declaração padrão definida na interface será ignorada. E mesmo novas classes que implementarem a interface não precisarão definir o método.

Conclusão

Não recomendo que isso seja utilizado em novas interfaces para que elas se enquadrem em várias situações. A recomendação de sempre criar uma interface enxuta e objetiva não mudou. Mas este recurso será muito útil na hora da manutenção dos códigos.

Mesmo assim, lembre-se de sempre utilizá-lo com parcimônia. Dependendo da situação, talvez a melhor opção seja criar uma nova interface e não alterar uma existente.

.NET Core 3.0 – Criando componentes no Razor Components

Em um artigo passado, falei sobre o Razor Components. Este novo tipo de aplicação, presente no .NET Core 3.0. Como dito anteriormente, o Razor Components é uma evolução do Blazor. Uma forma de criar componentes web no C#. Uma analogia seria com os componentes de frameworks JavaScript, como React e Angular.

Assim como nesses frameworks, os componentes do Razor encapsulam toda a lógica. Podendo ser adicionados em qualquer aplicação ASP.NET.

C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

Para compreender isso, vamos colocar a mão na massa.

Este artigo está sendo escrito com base nas definições do NET Core 3 Preview 3, então no futuro algumas informações não podem ser compatíveis com a versão mais atual do .NET Core. E os exemplos demostrados aqui, requerem esta versão do framework ou uma superior.

Estrutura de um componente Razor

Pelo terminal, é possível criar uma aplicação Razor Components com o comando abaixo:

dotnet new razorcomponents -n RazorApp

Na aplicação criada, você notará uma pasta chamada Components e dentro dela localizará arquivos com a extensão *.razor:

Esses arquivos são os componentes do Razor. Ao verificar o código de um componente, teremos:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

O início (@page "/counter") define a URL do componente. Dentro dela há códigos HTML e um bloco com a anotação @functions:

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Como o nome indica, no bloco desta anotação são definidas funções invocadas pelos componentes. Elas podem ser referenciadas no HTML do arquivo através da arroba (@) como no trecho abaixo:

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

Qualquer código definido neste ponto pode ser referenciado no HTML com a arroba, como é o caso da variável currentCount:

<p>Current count: @currentCount</p>

Ao executar a aplicação (dotnet run), podemos acessar o componente em /counter e visualizar o seu comportamento:

Criando um componente/página Razor

Agora que conhecemos um pouco da estrutura de um componente Razor vamos criar o nosso. Este primeiro componente será uma página, então dentro da pasta Components/Pages, adicione um arquivo chamado Posts.razor e nele adicione o código abaixo:

@page "/posts"
@using RazorApp.Services
@inject BlogService blogService

<h1>Posts</h1>

@if (posts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    @foreach (var post in posts)
    {
        <div class="card" style="width: 36rem;">
            <div class="card-body">
                <h1 class="card-title">@post.Title</h1>
                <p class="card-text">@post.Content</p>
            </div>
        </div>
    }
}

@functions {
    Post[] posts;

    protected override async Task OnInitAsync()
    {
        posts = await blogService.GetPostsAsync();
    }
}

Note que nesta página estamos definindo a listagem de posts:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
    </div>
}

Dados que virão de um service (BlogService), chamado na criação da página:

protected override async Task OnInitAsync()
{
    posts = await blogService.GetPostsAsync();
}

Assim, é necessário definir este service:

namespace RazorApp.Services
{
    public class BlogService 
    {
        public Task<Post[]> GetPostsAsync()
        {
            return Task.FromResult<Post[]>(new Post[]
            {
                new Post { Title = "Post 1", Content = "Conteúdo do Post 1" },
                new Post { Title = "Post 2", Content = "Conteúdo do Post 2" },
                new Post { Title = "Post 3", Content = "Conteúdo do Post 3" },
                new Post { Title = "Post 4", Content = "Conteúdo do Post 4" }
            });
        }
    }
}

Que contém apenas dados de exemplo. Em uma aplicação real, esta informação poderia vir do banco e/ou uma API.

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

Também será necessário definir o model Post:

public class Post 
{
    public string Title { get; set; }
    public string Content { get; set; }
}

“Injetar” o service na classe Startup:

public void ConfigureServices(IServiceCollection services)
{
    //Código omitido

    services.AddSingleton<BlogService>();
}

Por fim, no arquivo NavMenu.razor (presente na pasta Components/Shared), adicione um link para a página que acabamos de definir:

<li class="nav-item px-3">
    <NavLink class="nav-link" href="posts">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Posts
    </NavLink>
</li>

Ao acessar a aplicação, teremos acesso a esta página:

Simples, não é?

Criando um componente reutilizável

Agora que vimos como criar uma página, veremos como criar um componente reutilizável, que poderá ser adicionado em outras páginas e/ou componentes da aplicação.

Assim como o anterior, este componente será criado dentro da pasta Components/Pages, agora com o nome de LikeButton.razor e nele adicione o código abaixo:

<button onclick="@Liked" 
        class="btn @( likedCount == 0 ? "btn-outline-secondary" : "btn-outline-danger" )">
    <i class="far fa-heart"></i>
    @likedCount
</button>

@functions {
    [Parameter] int InitialLikedCount { get; set; }

    int likedCount = 0;

    protected override void OnInit()
    {
        likedCount = InitialLikedCount;
    }

    void Liked()
    {
        likedCount++;
    }
}

Neste componente estamos definindo um botão:

<button onclick="@Liked" 
        class="btn @( likedCount == 0 ? "btn-outline-secondary" : "btn-outline-danger" )">
    <i class="far fa-heart"></i>
    @likedCount
</button>

Este botão contém o ícone de coração do font-awesome, então é necessário adicionar a referência dela na página Index.cshml (localizada em Pages). No bloco @functions, é definido um parâmetro:

[Parameter] int InitialLikedCount { get; set; }

Que será atribuído a variável likedCount na inicialização da página:

protected override void OnInit()
{
    likedCount = InitialLikedCount;
}

Como é possível notar, ao definir [Parameter] , indicamos que o valor da variável irá como um atributo do componente.

Já no método Liked a variável likedCount é incrementada:

void Liked()
{
    likedCount++;
}

Para utilizar este componente, basta defini-lo em outra página como uma tag:

<LikedButton>

Faça isso no componente que criamos anteriormente:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
        <div class="card-footer">
            <LikeButton />
        </div>
    </div>
}

Inicialmente não estamos passando o parâmetro para o componente, mesmo assim ele funciona:

Caso seja passado o parâmetro:

@foreach (var post in posts)
{
    <div class="card" style="width: 36rem;">
        <div class="card-body">
            <h1 class="card-title">@post.Title</h1>
            <p class="card-text">@post.Content</p>
        </div>
        <div class="card-footer">
            <LikeButton InitialLikedCount="5" />
        </div>
    </div>
}

O componente será inicializado com o valor informado:

C# (C Sharp) Avançado
Curso de C# (C Sharp) Avançado
CONHEÇA O CURSO

Conclusão

Este é um exemplo simples de criação e reutilização de componentes, mas nele é possível notar o poder deste recurso. Tenho certeza que não irá demorar muito para termos componentes para ecossistemas específicos, como Bootstrap, Material, etc.

Afinal: o que é de fato o LINQ?

Certamente, uma das features mais interessantes e legais que o .NET e seu ecossistema oferece é o LINQ. LINQ é um acrônimo para Language Integrated Query, ou Consulta Integrada à Linguagem . Trata-se de um “framework” dentro do .NET destinado a auxiliar os desenvolvedores a escrever expressões de consulta diretamente em C# de maneira agnóstica.

C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

Mas o que exatamente é o LINQ?

Para entender melhor: pense nas seguintes hipóteses dentro de um software:

  • Se o software em questão precisar realizar consultas em um banco de dados relacional, provavelmente será necessário escrever consultas SQL;
  • Se o software em questão precisar realizar consultas em um arquivo XML, provavelmente será necessário utilizar expressões XPath e/ou XQuery;
  • Se o software em questão precisar realizar consultas em coleções de objetos, provavelmente será necessário utilizar blocos de código com a linguagem na qual o software está sendo desenvolvido.

Essa complexidade é adicionada pelo fato de estarmos tratando de fontes de dados heterogêneas de dados, cada qual com suas linguagens de manipulação específicas. Ou seja: se seu software se conecta a uma base de dados relacional e manipula objetos, você provavelmente precisará desenvolver e dar manutenção em pelo menos três linguagens diferentes: SQL, XPath (ou XQuery) e a linguagem utilizada para desenvolver a aplicação em si.
O LINQ tem como objetivo justamente remover essa complexidade, pois ele abstrai a complexidade envolvida na utilização de diferentes linguagens de consulta, como SQL, xPath e xQuery. Essa abstração é feita em cima de uma API de alto nível compatível com as linguagens integrantes do .NET Framework. Ou seja: você consegue consultar uma base de dados relacional, um arquivo XML uma coleção de objetos através de uma API unificada, invocada através de uma linguagem integrante do .NET Framework. Trazendo para um exemplo mais palpável: você consegue unicamente com código C# fazer consultas a conjuntos de objetos, bases de dados relacionais e arquivos XML, sendo o LINQ o encarregado de fazer a devida “tradução” para cada uma das fontes a serem consultadas.

Providers LINQ e árvores de expressão (expression trees)

O LINQ consegue trabalhar de maneira agnóstica (ou seja, a mesma API do LINQ consegue trabalhar em cima de várias fontes de dados diferentes) principalmente por causa de dois pilares: os providers e as árvores de expressão – ou expression trees. Para entendermos cada um deles, vamos a partir deste ponto adotar o C# como a linguagem de referência.

C# (C Sharp) Intermediário
Curso de C# (C Sharp) Intermediário
CONHEÇA O CURSO

De maneira geral e simplificada, uma expression tree é um trecho de código organizado como uma árvore onde cada nó pode corresponder a um valor, uma chamada de método ou até mesmo um operador aritmético ou lógico. No .NET, as árvores de expressão são definidas pela classe Expression, advinda do namespace System.Linq.Expressions.
Vamos montar uma árvore de expressão simples: uma árvore que retorna true caso receba um número menor que 5 ou retorna false no caso contrário. Para isso, precisamos criar um delegate que seja capaz de realizar essa análise… No .NET, existem dois tipos de delegate especializados e já disponibilizados pela API do LINQ:

  • Func: é um delegate que indica que um valor vai ser retornado. Delegates do tipo Func podem não receber parâmetros, mas sempre tem que prover algum retorno;
  • Action: é um delegate que indica que nenhum valor será retornado. Delegates do tipo Action podem não receber parâmetros e nunca irão retornar um valor.

Se verificarmos a situação acima, veremos que um delegate do tipo Func com um parâmetro de entrada do tipo int e uma saída do tipo boolean é o mais adequado. Sendo assim, utilizando expressões-lambda, podemos escrever esta definição:

Func<int, bool> expr = num => num < 5;

Para que este delegate se torne uma árvore de expressão de verdade, é necessário que ele seja envolvido pela classe Expression:

Expression<Func<int, bool>> expr = num => num < 5;

Agora, temos uma árvore de expressão completa com três ramificações:

  • O argumento do tipo int de entrada;
  • O operador de comparação &gt;, revelando que se trata de uma árvore de expressão binária;
  • O 5, um valor constante que deve ser utilizado para comparação.

Qualquer expressão LINQ é decomposta nestas árvores de expressão. Neste momento, entra em cena os providers LINQ.
Os providers LINQ são utilizados basicamente para fazer a tradução das árvores de expressão para cada uma das fontes de dados na qual a expressão LINQ está conectada. Os principais providers LINQ são:

  • LINQ to objects: utilizado quando a fonte de consulta é um conjunto de objetos;
  • LINQ to XML: utilizado quando a fonte de consulta é um XML;
  • LINQ to SQL: utilizado quando a fonte de consulta é um banco de dados relacional.

As principais estruturas dos providers LINQ são as interfaces IQueryProvidere IQueryable. Todos os providers LINQ e suas variações são obrigados a implementar estas interfaces.
Através das implementações dos providers LINQ, as expressões de consulta podem ser traduzidas para as suas respectivas fontes de dados… Por exemplo: vamos considerar a árvore de expressão anterior (Expression expr = num =&gt; num &lt; 5) com o número 4 como entrada.

Se estivéssemos falando de LINQ to Objects, cada nó que faz parte da expressão seria traduzido para o código C# correspondente. Teríamos o resultado abaixo:

Se estivéssemos falando de LINQ to SQL, cada nó que faz parte da mesma expressão seria traduzido para o código SQL correspondente. Teríamos o resultado abaixo:

Já se estivéssemos falando de LINQ to XML, cada nó que faz parte da mesma expressão seria traduzido para o código xPath/xQuery correspondente. Teríamos o resultado abaixo:

Veja que o LINQ, de acordo com a fonte de dados que está sendo analisada, utiliza o provider para realizar uma "tradução" da API de alto nível em C# para o código correspondente ao tipo da fonte de dados que está sendo utilizada. E esse processo torna a API completamente independente da fonte de dados a qual esta se conecta: quem fica responsável por dizer ao LINQ como cada nó da árvore de expressão tem que ser traduzido é o provider.

Existem muito mais coisas do LINQ a serem exploradas!

O LINQ é de fato uma ferramenta muito poderosa quando utilizada corretamente (especialmente quando estamos falando do LINQ to SQL). Este primeiro post na verdade tem a intenção de mostrar os princípios básicos e o funcionamento interno do LINQ. Nos próximos posts, iremos começar a analisar alguns pontos interessantes para quem utiliza o LINQ no dia-a-dia.

Criando um provider customizado para o Microsoft.Extensions.Logging

Caso já tenha desenvolvido uma aplicação ASP.NET Core, deve ter deparado com o seu sistema de log. Adicionado desde a primeira versão desta biblioteca, o ILoggingBuilder da Microsoft.Extensions.Logging é o responsável por registrar nos Services provedores de log para este tipo de aplicação.

Internamente, o provedor recebe do LoggerFactory todas as informações geradas durante a execução da aplicação, tanto em tempo de desenvolvimento quanto em produção.

Mesmo com uma série de provedores (nativos e de terceiros), em alguns casos é necessário criar um provedor customizado e neste artigo veremos como isso pode ser feito.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Anteriormente em um projeto ASP.NET Core…

Antes de vermos a criação do provedor, um resumo da biblioteca Microsoft.Extensions.Logging.

Esta biblioteca foi criada como uma API de log, que os desenvolvedores podem utilizar para capturar os logs internos do ASP.NET Core, bem como gerar logs customizados. Esses logs são enviados para provedores, que ficam responsáveis de registrar as mensagens.

No momento, há os seguintes provedores nativos:

E também temos os seguintes provedores de bibliotecas de terceiro:

Caso nenhuma dessas bibliotecas atendam as suas necessidades, chega a hora de criar um provedor customizado, é o que faremos a seguir.

Primeiro Ato – Preparando o ambiente

Neste exemplo, iremos criar um provedor que registre os logs dos dados em uma tabela do SQLite, então antes de criá-lo precisamos preparar o ambiente.

Neste artigo estou utilizando uma aplicação ASP.NET Core 2.2. Então, após a criação dela, adicione a biblioteca do SQLite:

dotnet add package System.Data.SQLite

E do Dapper (caso queira, você pode optar por outra biblioteca ORM):

dotnet add package Dapper

Já a configuração desta conexão, está seguindo o mesmo padrão do meu artigo do Dapper, que você pode ver aqui. A diferença é que para o provedor será definida a entidade/model abaixo:

public class EventLog
{
    public int Id { get; set; }
    public string Category { get; set; }
    public int EventId { get; set; }
    public string LogLevel { get; set; }
    public string Message { get; set; }
    public DateTime CreatedTime { get; set; }
}

No próximo tópico veremos a implementação do repositório desta entidade.

Segundo Ato – Criando o provedor

Para criar um provedor para o LoggerFactory é necessário criar ao menos duas classes, que implementem, respectivamente, as interfaces ILogger e ILoggerProvider. Também é recomendado a criação de um método de extensão, seguindo o padrão Add{nome provedor}, que facilitará a sua adição.

E como implementaremos um provedor para o SQLite, implementaremos um repositório abstrato. Este repositório será uma propriedade da nossa classe “ILogger. Assim, de início defina este repositório:

public abstract class LoggerRepository : AbstractRepository<EventLog>
{
    public LoggerRepository(IConfiguration configuration) : base(configuration){}
}

Esta classe herda a classe AbstractRepository que demonstrei no artigo do Dapper.

Aproveitando, já defina a EventLogRepository:

public class EventLogRepository : LoggerRepository
{
    public EventLogRepository(IConfiguration configuration) : base(configuration){}

    public override void Add(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "INSERT INTO EventLog (Category, EventId, LogLevel, Message, CreatedTime)"
                            + " VALUES(@Category, @EventId, @LogLevel, @Message, @CreatedTime)";
            dbConnection.Open();
            dbConnection.Execute(sQuery, item);
        }
    }

    public override IEnumerable<EventLog> FindAll()
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            dbConnection.Open();
            return dbConnection.Query<EventLog>("SELECT * FROM EventLog");
        }
    }

    public override EventLog FindByID(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "SELECT * FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            return dbConnection.Query<EventLog>(sQuery, new { Id = id }).FirstOrDefault();
        }
    }

    public override void Remove(int id)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "DELETE FROM EventLog" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Execute(sQuery, new { Id = id });
        }
    }

    public override void Update(EventLog item)
    {
        using (IDbConnection dbConnection = new SQLiteConnection(ConnectionString))
        {
            string sQuery = "UPDATE EventLog SET Category = @Category,"
                        + " EventId = @EventId, LogLevel= @LogLevel," 
                        + " Message = @Message, CreatedTime= @CreatedTime" 
                        + " WHERE Id = @Id";
            dbConnection.Open();
            dbConnection.Query(sQuery, item);
        }
    }
}

Agora podemos definir a nossa classe ILogger, que será chamada SqliteLogger:

public class SqliteLogger<T> : ILogger where T: LoggerRepository
{
    private Func<string, LogLevel, bool> _filter;
    private T _repository;
    private string _categoryName;
    private readonly int maxLength = 1024;
    private IExternalScopeProvider ScopeProvider { get; set; }

    public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
    {
        _filter = filter;
        _repository = repository;
        _categoryName = categoryName;
    }

    public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

    public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        var logBuilder = new StringBuilder();

        if (!IsEnabled(logLevel)) 
        { 
            return; 
        } 
        if (formatter == null) 
        { 
            throw new ArgumentNullException(nameof(formatter)); 
        } 
        var message = formatter(state, exception);
        if (!string.IsNullOrEmpty(message)) 
        { 
            logBuilder.Append(message);
            logBuilder.Append(Environment.NewLine);
        }

        GetScope(logBuilder);

        if (exception != null)
            logBuilder.Append(exception.ToString());

        if (logBuilder.Capacity > maxLength)
            logBuilder.Capacity = maxLength;

        var eventLog = new EventLog 
        { 
            Message = message, 
            EventId = eventId.Id,
            Category = _categoryName,
            LogLevel = logLevel.ToString(), 
            CreatedTime = DateTime.UtcNow 
        };

        _repository.Add(eventLog);
    }

    private void GetScope(StringBuilder stringBuilder)
    {
        var scopeProvider = ScopeProvider;
        if (scopeProvider != null)
        {
            var initialLength = stringBuilder.Length;

            scopeProvider.ForEachScope((scope, state) =>
            {
                var (builder, length) = state;
                var first = length == builder.Length;
                builder.Append(first ? "=> " : " => ").Append(scope);
            }, (stringBuilder, initialLength));

            stringBuilder.AppendLine();
        }
    }
}

Note que esta é uma classe parametrizada, que define que o tipo de dados deve herdar a classe LoggerRepository:

public class SqliteLogger<T> : ILogger where T: LoggerRepository

No construtor da classe, é recebido um predicado, o repositório e a categoria:

public SqliteLogger(Func<string, LogLevel, bool> filter, T repository, string categoryName)
{
    _filter = filter;
    _repository = repository;
    _categoryName = categoryName;
}

O predicado é um filtro que definirá qual nível de log deve ser registrado. Já a categoria é o valor definido na criação do logger.

No método BeginScope, adicionamos os escopos definidos em um provider de escopo:

public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;

Esses escopos são iterados e posteriormente adicionados na mensagem que será salva no banco.

No método IsEnabled:

public bool IsEnabled(LogLevel logLevel) => (_filter == null || _filter(_categoryName, logLevel));

Verifica, de acordo com o filtro, se o log deve ser registrado ou não.

Já no método Log é onde montamos a nossa mensagem e a salvamos no banco de dados (através do repositório).

Com o SqliteLogger definido, podemos criar o provedor em si:

public class SqliteLoggerProvider<T>: ILoggerProvider where T : LoggerRepository
{
    private readonly Func<string, LogLevel, bool> _filter;
    private readonly T _repository;

    public SqliteLoggerProvider(Func<string, LogLevel, bool> filter, T repository)
    {
        this._filter = filter;
        this._repository = repository;
    }

    public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

    public void Dispose() {}
}

Esta classe também recebe no seu construtor um predicado de filtro e o repositório. Já no método CreateLogger é retornado uma instância do nosso logger, passando a categoria informada:

public ILogger CreateLogger(string categoryName) => new SqliteLogger<T>(_filter, _repository, categoryName);

Agora é necessário definir o método de extensão:

public static class SqliteLoggerExtensions
{
    public static ILoggingBuilder AddSqliteProvider<T>(this ILoggingBuilder builder, T repository) where T: LoggerRepository
    {
        builder.Services.AddSingleton<ILoggerProvider, SqliteLoggerProvider<T>>(p => new SqliteLoggerProvider<T>((_, logLevel) => logLevel >= LogLevel.Debug, repository));

        return builder;
    }
}

Neste método de extensão é definido que o level mínimo do log é o Debug, que é o mais baixo. Isso significa que todas as mensagens de log serão registrados.

Terceiro Ato – A hora da verdade

Com o provedor definido, podemos utilizá-lo. Na atual versão do ASP.NET Core (2.2), isso deve ser feito no método CreateWebHostBuilder da classe Program, conforme abaixo:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddSqliteProvider(new EventLogRepository(hostingContext.Configuration));
        });

Ao executar a aplicação, todos os logs do ASP.NET Core serão salvos no nosso banco de dados:

Qualquer log definido dentro da aplicação, como o abaixo:

public class HomeController : Controller
{
    private readonly ILogger _logger;

    public HomeController(ILogger<HomeController> logger) => _logger = logger;

    public IActionResult Index()
    {
        _logger.LogInformation("Chamando a página inicial do site");

        return View();
    }

    //...
}

Também será salvo no banco.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Epílogo

Mesmo com várias opções de provedores para o Microsoft.Extensions.Logging, às vezes há a necessidade de criar um provedor customizado. Felizmente, como vimos, a criação deste tipo de provedor não é complexa. Então, quando não achar uma solução que o atende, opte pela implementação customizada.

Você pode ver o código da aplicação utilizada neste artigo no meu Github, aqui.

.NET Core 3.0 – Razor Components

No final do mês de janeiro, foi lançado o segundo preview do .NET Core 3.0. Nele foi introduzido o Razor Components, conhecido anteriormente como Blazor server-side.

Para os que não conhecem, o Blazor é um projeto experimental da Microsoft que tem por objetivo levar uma aplicação .NET ao navegador utilizando WebAssembly. Pense em uma aplicação Angular ou React, mas utilizando C# e Razor.

No momento o Blazor trata-se apenas de um experimento, mas a equipe do ASP .NET pretende adicionar na versão final do .NET Core 3.0 o Razor Components.

Então vamos conhecê-lo.

Razor Components

O desenvolvimento web tem evoluído de várias maneiras ao longo dos últimos anos. Hoje há uma infinidade de frameworks front-end e N formas de se criar aplicações web. Mesmo assim, ainda há desafios na criação de uma aplicação web moderna.

Com o objetivo de sanar alguns desses desafios que se iniciou o “experimento” Blazor e dele, agora temos o Razor Components.

Funcionamento

Diferente do Blazor, no Razor Components o código C# não é executado no navegador. Ele é processado no servidor, que se comunica com a interface através do SignalR:

Na prática, ao acessar uma aplicação criada com o Razor Components, ela se comportará como uma SPA (Single Page Application). Será baixado um arquivo index.html e um arquivo Javascript chamado blazor.server.js. Em seguida a aplicação estabelecerá uma conexão com o servidor através do SignalR.

No servidor, o componente da página será processado gerando o seu HTML. Este é comparado “instantaneamente” com a versão presente no client. Caso haja alguma diferença, ela é comparada, as alterações são enviadas para o client, o navegador “desempacota” o pacote e aplica as alterações no DOM do client.

Quando há alguma interação com a tela (como o clique de um botão), o evento é comparado e enviado para o servidor pela mesma conexão. Onde ele é processado e retorna o resultado, com as alterações que devem ser aplicadas ao DOM.

C# (C Sharp) - ASP.NET MVC
Curso de C# (C Sharp) - ASP.NET MVC
CONHEÇA O CURSO

Vantagens

Há várias vantagens em utilizar o Razor Components:

  • .NET Core APIs: Por executar no servidor, a aplicação pode fazer uso das várias APIs disponíveis no .NET Core. Além de reaproveitar códigos de outras aplicações C#, se for necessário. Sendo possível até utilizar os componentes do Razor em aplicações ASP.NET MVC (no momento o inverso ainda não é possível);
  • Aparência de SPA: Como disse antes, uma aplicação Razor Components se comporta como uma aplicação SPA, mesmo que não seja desenvolvida como tradicionalmente este tipo de aplicação é criada;
  • Estabilidade e performance: A equipe do .NET Core procura sempre melhorar a performance do framework. Então ao utilizá-lo, já há a garantia que a aplicação será performática, estável e segura;
  • Rapidez no carregamento: Comparado com o Blazor, a aplicação do Razor Components envia para o client apenas dois arquivos leves, que são baixados rapidamente;
  • Suporte de vários navegadores: A principal desvantagem do Blazor é o suporte apenas dos navegadores que suportam o WebAssembly. Já o Razor Components é suportado praticamente por todos os navegadores, incluindo navegadores móveis.

Desvantagens

Infelizmente não existe almoço grátis, o Razor Components também possui algumas desvantagens:

  • Online only: Como o código C# é executado no servidor, a aplicação necessita sempre se comunicar com ele quando ocorrer qualquer tipo de interação na página. Assim, não é possível gerar uma versão offline da aplicação;
  • Latência: Mesmo o SignalR sendo muito eficiente, ainda pode haver alguns engasgos na comunicação do client com o servidor. Principalmente porque qualquer interação do usuário com a aplicação precisa ser enviada para o servidor, processada e um DOM resultante retornado;
  • Persistência do estado da aplicação: No momento se a comunicação com o servidor for perdida, o estado da aplicação também será perdido. E por enquanto não há muitas soluções para este cenário.

Encerrando

Vou finalizar este artigo por aqui. Agora que você conhece um pouco do Razor Components, no próximo vou mostrar um pouco de código. Veremos como criar este tipo de aplicação e a criação de alguns componentes.

Até a próxima.