EF Core - Sistema de Vendas : Usando a Fluent API - II


 Neste artigo eu vou mostrar como usar os recursos da Fluent API com EF Core em um projeto Console.


Continuando a primeira parte do artigo vamos agora definir o contexto e o mapeamento aplicando a Fluent API.


Definindo o contexto e o mapeamento com Fluent API

No projeto Console vamos definir agora o contexto e a seguir vamos criar o mapeamento usando a Fluent API.

Vamos criar 2 pastas no projeto :

Definindo o contexto e configurando as entidades

Abra a pasta Context e crie nesta pasta a classe ApplicationDbContext que herda de DbContext.

Nesta classe vamos usar o método OnConfiguring() onde vamos obter uma instância de DbContextOptionsBuilder para definir o provedor do banco de dados e a string de conexão.

Nesta classe iremos definimos também o mapeamento entre as entidade do domínio e o banco de dados usando a classe DbSet<T> e a Fluente API.

No método OnModelCreating vamos usar o método ApplyConfigurationsFromAssembly para aplicar algumas configurações que iremos fazer nas entidades definidas neste assembly.

Essas configurações devem ser criadas em um arquivo separado e implementar a interface IEntityTypeConfiguration<T>. Estamos fazendo isso para organizar o código do nosso projeto.

Nota:  O método OnModelCreating é chamado quando seu contexto é criado pela primeira vez para construir o modelo e seus mapeamentos na memória;

1- ApplicationDbContext

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Context
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Pedido> Pedidos { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Sua_String_de_Conexao");
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
        }
    }
}

Neste arquivo note que definimos apenas uma propriedade do tipo DbSet<Pedido> para Pedidos.

Ocorre que como a classe Pedido contém propriedades de navegação para Cliente e Item e esta contém a referência para Produto todas as classes serão mapeadas pelo EF Core que vai inferir que existe o relacionamento entre as classes. Isso já é suficiente para gerar todas as tabelas relacionadas com as entidades.

A seguir vamos criar a pasta EntityConfiguration no projeto e nesta pasta vamos criar as classes onde vamos definir as configurações usando a Fluent API para as entidades do contexto.

Vamos começar com a entidade Cliente criando a classe ClienteConfiguration que implementa a interface IEntityTypeConfiguration<Cliente>

2- ClienteConfiguration

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Infrastructure.EntityConfiguration
{
    public class ClienteConfiguration : IEntityTypeConfiguration<Cliente>
    {
        public void Configure(EntityTypeBuilder<Cliente> builder)
        {
            builder.ToTable("Clientes");
            builder.HasKey(t => t.ClienteId);
            builder.Property(p => p.Nome).HasColumnType("VARCHAR(80)").IsRequired();
            builder.Property(p => p.Email).HasColumnType("VARCHAR(150)").IsRequired();
            builder.Property(p => p.Telefone).HasColumnType("CHAR(11)");
            builder.Property(p => p.Cep).HasColumnType("CHAR(8)").IsRequired();
            builder.Property(p => p.Estado).HasColumnType("CHAR(2)").IsRequired();
            builder.Property(p => p.Cidade).HasMaxLength(100).IsRequired();

            builder.HasIndex(i => i.Email).HasDatabaseName("idx_cliente_email");
            builder.HasIndex(i => i.Telefone).HasDatabaseName("idx_cliente_telefone");

            builder.HasMany(p => p.Pedidos)
                .WithOne(p => p.Cliente)
                .OnDelete(DeleteBehavior.Cascade);

            builder.HasData(
              new Cliente
              {
                  ClienteId = 1,
                  Nome = "Maria",
                  Email = "maria@teste.com",
                  Telefone = "55-9985-5555",
                  Cep = "03258-000",
                  Estado = "SP",
                  Cidade = "São Paulo"
              }

           );
        }
    }
}

Na implementação do método Configure usamos uma instância de EntityTypeBuilder<T> para definir as configurações que o EF Core vai usar quando aplicar o Migrations e também usamos a propriedade HasData que será usada para popular a tabela Cliente no banco de dados com os dados definidos.

Além disso ao definir o mapeamento para as propriedades usamos os seguintes métodos de extensão da Fluent API:

- builder.ToTable("Clientes");
Mapeia a entidade para a tabela Clientes

- builder.Property(p => p.Nome).HasColumnType("VARCHAR(80)").IsRequired();
Define a propriedade Nome para ser mapeada para uma coluna do tipo VARCHAR(80) onde a informação é obrigatória

- builder.Property(p => p.Cidade).HasMaxLength(100).IsRequired();
Mapeia a propriedade Cidade para a coluna com tamanho de 100 caracteres e do tipo NVARCHAR obrigatória

- builder.HasIndex(i => i.Email).HasDatabaseName("idx_cliente_email");
- builder.HasIndex(i => i.Telefone).HasDatabaseName("idx_cliente_telefone");
Define dois índices na tabela para email e telefone para otimizar as consultas

- builder.HasMany(p => p.Pedidos)
      .WithOne(p => p.Cliente)
      .OnDelete(DeleteBehavior.Cascade);

O método HasMany define o relacionamento um-para-muitos entre Cliente e Pedidos onde um Cliente possui muitos Pedidos. Aqui é preciso seguir o padrão
Has/With e emparelhar o uso de HasMany com o método WithOne.

Para concluir usamos o método OnDelete  para especificar a ação que deve ocorrer em uma entidade dependente em um relacionamento quando o principal é excluído.

O método OnDelete usa um enum DeleteBehavior como parâmetro:

  1. Cascade - dependentes devem ser excluídos
  2. Restringir - os dependentes não são afetados
  3. SetNull - os valores da chave estrangeira nas linhas dependentes devem ser atualizados para NULL

Se o banco de dados foi criado a partir de migrações EF Core, o comportamento especificado é configurado automaticamente.

A propriedade HasData foi usada para popular a tabela Clientes com dados iniciais. Iremos repetir essa abordagem para as demais classes a serem configuradas.

Vamos repetir esse procedimento criando a classe ProdutoConfiguration:

3- ProdutoConfiguration

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Infrastructure.EntityConfiguration
{
    public class ProdutoConfiguration : IEntityTypeConfiguration<Produto>
    {
        public void Configure(EntityTypeBuilder<Produto> builder)
        {
            builder.ToTable("Produtos");
            builder.HasKey(t => t.ProdutoId);
            builder.Property(p => p.Nome).HasColumnType("VARCHAR(80)").IsRequired();
            builder.Property(p => p.Descricao).HasColumnType("VARCHAR(150)").IsRequired();
            builder.Property(p => p.Preco).HasPrecision(10,2).IsRequired();
            builder.Property(p => p.Ativo).IsRequired();

            builder.HasMany(p => p.Itens)
                .WithOne(p => p.Produto)
                .OnDelete(DeleteBehavior.Cascade);

            builder.HasData(
              new Produto
              {
                  ProdutoId = 1,
                  Nome = "Caderno espiral",
                  Descricao = "Caderno espiral 100 folhas",
                  Preco = 9.55M,
                  Ativo = true
              }
           );

        }
    }
}

Nesta configuração destacamos o uso do método HasPrecision que altera a precisão da coluna decimal para 10 dígitos com duas casas decimais.

4- ItemConfiguration

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Infrastructure.EntityConfiguration
{
    internal class ItemConfiguration : IEntityTypeConfiguration<Item>
    {
        public void Configure(EntityTypeBuilder<Item> builder)
        {
            builder.ToTable("Itens");
            builder.HasKey(t => t.ItemId);
            builder.Property(p => p.Quantidade).HasDefaultValue(1).IsRequired();
            builder.Property(p => p.Preco).HasPrecision(10, 2).IsRequired();

            builder.HasData(
              new Item
              {
                  ItemId = 1,
                  PedidoId = 1,
                  ProdutoId = 1,
                  Quantidade = 1,
                  Preco = 7.45M
              }

           );
        }
    }
}

Nesta configuração destacamos o uso do método HasDefaultValue(1) que permite definir um valor padrão para a coluna mapeada.

5- PedidoConfiguration

using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;

namespace Infrastructure.EntityConfiguration
{
    internal class PedidoConfiguration : IEntityTypeConfiguration<Pedido>
    {
        public void Configure(EntityTypeBuilder<Pedido> builder)
        {
            builder.ToTable("Pedidos");
            builder.HasKey(t => t.PedidoId);
            builder.Property(p => p.DataPedido).HasDefaultValueSql("GETDATE()").ValueGeneratedOnAdd();
            builder.Property(p => p.Frete).HasConversion<int>();
            builder.Property(p => p.Status).HasConversion<string>();

            builder.HasMany(p => p.Itens)
                .WithOne(p => p.Pedido)
                .OnDelete(DeleteBehavior.Cascade);

            builder.HasData(
              new Pedido
              {
                  PedidoId = 1,
                  ClienteId = 1,
                  DataPedido = DateTime.Now,
                  Frete = TipoFrete.FOB,
                  Status = StatusPedido.Analise
              }

           );
        }
    }
}

Aqui neste código temos o uso dos métodos:

Para indicar ao EF Core que o mapeamento foi definido nas classes de configuração para cada entidade usamos o código abaixo no arquivo ApplicationDbContext:

builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

Com essa linha de código estamos informando ao EF Core para pesquisar as classes no assembly que contém a classe ApplicationDbContext.

Dessa forma já temos o contexto e o mapeamento das classes definidas usando a Fluent API e os dois pacotes principais do EF Core necessários para aplicar a migração que são os pacotes :

  1. Microsoft.EntityFrameworkcore.Tools
  2. Microsoft.EntityFrameworkcore.Design

Vamos aplicar a migração para gerar as tabelas e o banco de dados.

Aplicando o Migrations

Usando o recurso Migrations do Entity Framework podemos realizar alterações em nosso modelo de entidades e ter a atualização automática do banco de dados refletindo essas mudanças.

Através do Code First podemos gerar de forma automática o nosso modelo de dados a partir do modelo de entidades e com o Migrations podemos gerar atualizações no banco de dados refletindo as alterações porventura feitas  no modelo de entidades.

Dessa forma o Migrations pode ser usado com o Code First para automatizar de vez o processo de geração e atualização do modelo de dados com base no modelo de entidades onde o Migrations vai manter o modelo de dados gerado via Code First sempre atualizado com as classes do modelo de entidades.

Podemos usar o Migrations de forma totalmente automática onde todo o processo de atualização do banco de dados é feita de forma transparente pelo Migrations.

O processo de criar e aplicar o Migrations envolve duas etapas:

  1. Criar a migração - Cria o arquivo de script SQL contendo os comandos da migração;
  2. Aplicar a migração - Execute o arquivo de script gerado e aplica os comandos ao banco de dados;

Como já dissemos, nosso esquema de banco de dados deve estar alinhado com o modelo e todas as alterações em um modelo de  precisam ser migradas para o próprio banco de dados.

Para gerar o script de migração usamos o seguinte comando na janela do Package Manager Console:

add-migration <nome_da_migração> [options]

Este comando precisa do pacote Microsoft.EntityFrameworkCore.Tools instalado para funcionar.

Usando o VS Code e a ferramenta de linha de comando CLI o comando é:

dotnet ef migrations add NomeDaMigração [options]

Para este comando funcionar temos instalar a ferramenta de linha de comando do EF Core globalmente usando o comando:

dotnet tool install --global dotnet-ef

Vamos abrir o projeto ASP .NET Core e na janela do PMC digitar o comando: add-migration MigracaoInicial   

Veremos na pasta Migrations gerada no projeto a  classe MigracaoInicial que herda  de Migration e define dois métodos : Up e Down.

No método Up() temos os comandos para criar as tabelas e no método Down temos o comando para remover a tabela criada caso desejamos remover a migração.

Esses dois métodos são usados na classe que herda de Migration para definir as alterações que serão aplicadas ao banco de dados, sendo que no método Up() aplica o script e o método Down() reverte a aplicação feita.

Assim, se você cometeu algum erro e deseja remover a migração basta digitar: Remove-Migration o método Down será executado e a migração será revertida.

Note que é criada uma pasta Migrations no projeto contendo os arquivos de migração:

Vamos partir deste cenário mas vamos usar a abord

Aqui temos os arquivos:

Aplicando a migração

Depois de criar nossa migração com sucesso, precisamos aplicá-la para que as alterações entrem em vigor no banco de dados.

Existem várias maneiras de aplicar migrações (usando scripts SQL, usando o método Database.Migrate ou usando métodos de linha de comando) e, como fizemos na criação, vamos usar a abordagem de métodos de linha de comando.

Usando o VS 2019 na janela do PMC podemos emitir o comando: update-database [options]

Usando o VS Code e a ferramenta NET CLI o comando usado é : dotnet ef database update [options]

Para o nosso exemplo vamos digitar o comando : update-database

Ao final do processamento teremos o banco de dados VendasDemoDB e as tabelas criadas no SQL Server conforme as as configurações que definimos usando a Fluent API.

Abrindo o SQL Server Management Studio podemos confirmar a criação do banco de dados VendasDemoDb e das tabelas : Clientes, Itens, Pedidos e Produtos.

Além disso ao criarmos um diagrama das tabelas vemos as associações definidas nas classes sendo expressos nos relacionamentos entre as tabelas.

Além disso se abrirmos cada uma das tabelas veremos que elas já contém dados iniciais que foram incluídos na migração graças a propriedade HasData.

Agora, observe a tabela _EFMigrationsHistory que foi criada no banco de dados.

O EF Core usa esta tabela para rastrear todas as migrações aplicadas. Portanto, isso significa que, se criarmos outra migração em nosso código e aplicá-lo, o EF Core aplicará apenas a migração recém-criada.

Mas como o EF Core sabe qual migração precisa ser aplicada ?

Bem, ele armazena um ID exclusivo no _EFMigrationsHistory, que é um nome exclusivo do arquivo de migração criado com a migração e nunca executa arquivos com o mesmo nome novamente.

Com isso concluímos este artigo confirmando que a aplicação do mapeamento usando a Fluent API oferece muita flexibilidade e permite realizar diversas configurações de mapeamento.

Pegue o projeto aqui : Vendas.zip (sem as referências)

"Não me envergonho do evangelho, porque é o poder de Deus para a salvação de todo aquele que crê: primeiro do judeu, depois do grego."
Romanos 1:16

Referências:


José Carlos Macoratti