Criando um CRUD com Entity Framework

Criar um CRUD com EF Core é um dos primeiros passos de quem começa a trabalhar com acesso a dados no .NET. Neste artigo, vamos montar um CRUD completo com Entity Framework Core, de forma simples e objetiva, usando o .NET 7.

O que é o Entity Framework Core

O Entity Framework Core é um conjunto de bibliotecas para acesso a dados, criado e mantido pela Microsoft. Ele facilita a execução de operações CRUD no SQL Server e em outros bancos, como SQLite e PostgreSQL.

Segundo a documentação oficial do EF Core, ele é um mapeador objeto relacional, ou ORM. Em outras palavras, você trabalha com classes C# e o framework traduz tudo para SQL.

A grande diferença entre o Entity Framework e o Dapper é que o EF é um framework completo. Ou seja, ele abstrai muita coisa, inclusive a criação do banco e o código SQL.

O que é CRUD

CRUD é a sigla para CREATE (criar), READ (ler), UPDATE (atualizar) e DELETE (excluir). Em resumo, são as quatro operações básicas que fazemos nos bancos de dados.

O CRUD é conhecido como a base que todo programador precisa dominar. Inclusive, é quase certo que em alguma entrevista de emprego você terá que implementá-lo.

Por isso, entender como fazer um CRUD com EF Core vale tanto para projetos reais quanto para processos seletivos. Além disso, as mesmas ideias se repetem em APIs, aplicações web e serviços.

Ferramentas necessárias

Para seguir este tutorial, você precisa do .NET e do SQL Server instalados. Os artigos abaixo detalham a instalação dessas ferramentas:

Se preferir algo ainda mais simples, você também pode usar o SQLite. Nesse caso, o banco vira um arquivo local e dispensa servidor.

Por fim, qualquer editor de código serve para acompanhar os exemplos, como o Visual Studio, o VS Code ou o Rider.

Criando o projeto do CRUD com EF Core

Vamos começar criando um projeto novo com o comando dotnet new, conforme mostrado abaixo. Neste caso, estou usando o .NET 7.

dotnet new console -o EfCrud
cd EfCrud

O comando cria um projeto do tipo console na pasta EfCrud. Em seguida, entramos na pasta para executar os próximos comandos.

Vale lembrar que o .NET 7 já saiu do período de suporte da Microsoft. Mesmo assim, os passos do CRUD com EF Core são praticamente os mesmos nas versões mais novas.

Iniciando com Code First

O Entity Framework tem uma abordagem de início diferente da que vimos no artigo sobre como criar um CRUD com Dapper. Essa abordagem se chama Code First.

Como o nome sugere, o Code First nos diz para codificar primeiro e esquecer o banco de dados por enquanto. No entanto, também podemos começar pelo banco, sem problemas; essa é a abordagem Database First.

De qualquer forma, vamos criar nossa classe, ou como carinhosamente chamamos, nossa entidade. Ela vai representar uma tabela no banco.

namespace EfCrud.Entities;

public class Category
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Slug { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
}

Se você preferir o caminho Database First, o dotnet-ef também resolve. O comando dotnet ef dbcontext scaffold lê um banco existente e gera as entidades e o contexto automaticamente.

Neste tutorial, porém, seguimos com o Code First, que é o caminho mais comum para quem está começando. Além disso, ele deixa o modelo do domínio sob controle do código, e não do banco.

Por convenção, o EF Core entende que a propriedade Id é a chave primária. Além disso, como ela é do tipo int, o SQL Server gera o valor automaticamente a cada inserção.

Ferramentas do Entity Framework

Para gerar o banco e executar outros comandos do EF, precisamos instalar uma ferramenta adicional. Essa ferramenta é o dotnet-ef, instalado como ferramenta global do .NET.

# Primeira instalação
dotnet tool install --global dotnet-ef

# Se você já tem a ferramenta e quer apenas
# atualizar para a última versão
dotnet tool update --global dotnet-ef

Com ele, você cria migrações, atualiza o banco e até gera classes a partir de um banco existente. Portanto, essa é uma peça importante do CRUD com EF Core.

Instalando o Entity Framework

Na raiz do projeto, instalamos o Entity Framework executando um dos comandos abaixo. A escolha depende do banco que você vai usar.

# Caso queira trabalhar com SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

# Caso queira trabalhar com SQLite
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

# Necessário para executar os comandos do dotnet-ef no projeto
dotnet add package Microsoft.EntityFrameworkCore.Design

A parte boa do Entity Framework é que ele abstrai o banco para nós. Assim, tanto faz se você quer usar SQLite ou SQL Server.

Como o projeto usa .NET 7, informe com --version uma versão 7.0.x de cada pacote. Isso é necessário porque as versões 8 em diante do EF Core exigem o .NET 8.

Além disso, a documentação recomenda usar a ferramenta e os pacotes com a mesma versão principal. Dessa forma, você evita erros estranhos ao rodar as migrações.

Data context

No CRUD com EF Core, o contexto de dados é basicamente a representação do banco de dados em memória. Fazemos as operações nele e, depois, ele transforma tudo em SQL e persiste as informações no banco.

Criando um data context

Embora pareça complexo, um data context nada mais é do que uma classe que herda de DbContext. Feito isso, precisamos apenas informar quais entidades queremos relacionar com o banco.

using EfCrud.Entities;
using Microsoft.EntityFrameworkCore;

namespace EfCrud.Data;

public class AppDbContext : DbContext
{
    public DbSet<Category> Categories { get; set; } = null!;
}

Acredite ou não, isso é tudo o que precisamos para mapear o banco para um contexto de dados e vice-versa. Bem simples, não é?

Cada DbSet representa uma tabela. Nesse caso, a propriedade Categories se liga à entidade Category e vira a tabela Categories no banco.

Connection string

Até o momento, dissemos que teremos um banco com a tabela Categories. Porém, em nenhum momento dissemos qual é esse banco, em qual servidor ele está e como chegar até ele.

No CRUD com EF Core, isso é feito pela connection string, ou string de conexão. Ela nada mais é do que um texto separado por ; que informa os detalhes de acesso ao banco.

Se você não sabe como obter essa informação do seu SQL Server, veja este artigo sobre SQL Server no Docker. Inclusive, a connection string usada aqui saiu dele.

using EfCrud.Entities;
using Microsoft.EntityFrameworkCore;

namespace EfCrud.Data;

public class AppDbContext : DbContext
{
    public DbSet<Category> Categories { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("Server=localhost,1433;Database=EfCrud;User ID=sa;Password=1q2w3e4r@#$;Trusted_Connection=False;TrustServerCertificate=True;");
}

Caso queira trabalhar com SQLite, mudamos apenas o provedor e a connection string, conforme abaixo.

using EfCrud.Entities;
using Microsoft.EntityFrameworkCore;

namespace EfCrud.Data;

public class AppDbContext : DbContext
{
    public DbSet<Category> Categories { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("DataSource=app.db;Cache=Shared");
}

Para fins de estudo, deixar a senha no código do CRUD com EF Core é aceitável. Contudo, a própria documentação do EF Core alerta que connection strings não devem ficar no código em produção.

Gerando o banco do CRUD com EF Core

Com o contexto pronto, falta criar o banco e a tabela. Para isso, usamos as migrações do EF Core, que geram o esquema a partir das classes.

# Cria a migração inicial com base no AppDbContext
dotnet ef migrations add InitialCreate

# Cria o banco e aplica a migração
dotnet ef database update

O primeiro comando gera a pasta Migrations com o código que cria a tabela. Em seguida, o segundo comando executa esse código no banco configurado na connection string.

Sempre que você alterar uma entidade, crie uma nova migração e aplique no banco. O artigo sobre migrations com EF Core e PostgreSQL aprofunda esse fluxo.

Operações do CRUD com EF Core

Estamos na reta final. Agora, com tudo configurado, executamos as operações do CRUD com EF Core com muita facilidade.

Listei cada operação abaixo, da forma mais simples e objetiva possível. Lembre-se de adicionar using EfCrud.Data;, using EfCrud.Entities; e using Microsoft.EntityFrameworkCore; no Program.cs.

Create no CRUD com EF Core

A criação começa com uma instância da entidade. Em seguida, adicionamos essa instância ao contexto e salvamos as mudanças.

// Instância da entidade
var category = new Category
{
    Title = "Category 1",
    Slug = "category-1",
    Description = "Description of category 1"
};

// Instância do data context
using var context = new AppDbContext();

// Adiciona a entidade ao data context
await context.Categories.AddAsync(category);

// Persiste as mudanças
await context.SaveChangesAsync();

Dois pontos importantes aqui são o using e o await. O using fecha a conexão assim que ela deixa de ser usada, assim como no Dapper com o SqlConnection.

Já o await torna as operações assíncronas. Com isso, a thread fica livre enquanto o banco processa o comando.

Note também que precisamos de duas linhas para persistir os dados, e isso vale para salvar, excluir e atualizar. Isso acontece porque o data context é a representação do banco em memória.

Ou seja, o AddAsync adiciona a entidade apenas no banco em memória. Para salvar de fato no banco de dados, precisamos do SaveChangesAsync, e esse comportamento se repete nos outros métodos, exceto na leitura.

Um detalhe: a documentação explica que o AddAsync só acessa o banco com geradores de valor especiais, como o HiLo. Portanto, na maioria dos casos o Add síncrono é suficiente.

Read no CRUD com EF Core

Como você deve imaginar, ler os dados no CRUD com EF Core não é difícil, mas tem alguns detalhes. Tudo o que precisamos está em context.Categories, porém o resultado dessa propriedade é um IQueryable.

Em outras palavras, a consulta ainda não foi materializada. Ela só é executada no banco quando chamamos métodos como ToListAsync ou FirstOrDefaultAsync.

// Instância do data context
using var context = new AppDbContext();

// Lê todas as categorias
var categories = await context.Categories.ToListAsync();

// Busca a categoria com Id 1
var category = await context.Categories.FirstOrDefaultAsync(x => x.Id == 1);

Você percebeu que inverter o ToListAsync com um Where pode causar a leitura de todos os dados da tabela? Veja a diferença abaixo.

// ERRADO: carrega a tabela inteira e só depois filtra em memória
var all = await context.Categories.ToListAsync();
var wrong = all.Where(x => x.Title.Contains("Category")).ToList();

// CORRETO: o filtro vira um WHERE no SQL
var right = await context.Categories
    .Where(x => x.Title.Contains("Category"))
    .ToListAsync();

No primeiro caso, o filtro roda na aplicação, depois de trazer todas as linhas. Já no segundo, o EF Core traduz o Where para SQL e o banco devolve apenas o necessário.

Esse comportamento tem relação direta com a diferença entre IEnumerable e IQueryable. O artigo IEnumerable vs IQueryable explica isso em detalhes.

Para consultas somente leitura, o EF Core também oferece o AsNoTracking. Ele desliga o rastreamento de alterações e costuma deixar a leitura mais leve.

Update no CRUD com EF Core

Como os processos são parecidos, não vou me estender aqui. A diferença é que não existe um método UpdateAsync, então o Update não precisa de await.

// Instância do data context
using var context = new AppDbContext();

// Recupera a entidade (reidratação)
var category = await context.Categories.FirstOrDefaultAsync(x => x.Id == 1);

if (category is null)
    return;

// Altera a entidade
category.Title = "Updated category";

// Atualiza os dados no data context
context.Categories.Update(category);

// Persiste os dados no banco
await context.SaveChangesAsync();

Repare que adicionamos uma verificação de null. Afinal, o FirstOrDefaultAsync retorna null quando não encontra o registro.

Como a entidade veio de uma consulta com rastreamento, o EF Core já detecta a alteração sozinho. Nesse caso, o Update é opcional, mas deixa a intenção explícita no código.

Delete no CRUD com EF Core

Por fim, temos o método de exclusão, que também é bem simples. Assim como o Update, o Remove não tem versão assíncrona.

// Instância do data context
using var context = new AppDbContext();

// Recupera a entidade
var category = await context.Categories.FirstOrDefaultAsync(x => x.Id == 1);

if (category is null)
    return;

// Remove a entidade do data context
context.Categories.Remove(category);

// Persiste as mudanças
await context.SaveChangesAsync();

A documentação confirma que Update, Attach e Remove não têm sobrecargas assíncronas. Isso acontece porque esses métodos nunca precisam acessar o banco para gerar chaves.

ExecuteUpdate e ExecuteDelete no EF Core 7

O EF Core 7 trouxe os métodos ExecuteUpdateAsync e ExecuteDeleteAsync. Eles enviam um único comando ao banco, sem carregar as entidades em memória.

// Atualiza o título direto no banco, sem carregar a entidade
await context.Categories
    .Where(x => x.Id == 1)
    .ExecuteUpdateAsync(s => s.SetProperty(x => x.Title, "Updated category"));

// Exclui direto no banco, sem carregar a entidade
await context.Categories
    .Where(x => x.Id == 1)
    .ExecuteDeleteAsync();

Esses métodos executam imediatamente e não dependem do SaveChangesAsync. Por outro lado, eles não sincronizam entidades que já estejam rastreadas pelo contexto.

Por isso, a documentação diz que eles complementam o SaveChanges no CRUD com EF Core, e não o substituem. Use quando precisar alterar ou excluir muitos registros de uma vez.

Executando o CRUD com EF Core no Program.cs

Para ver tudo funcionando, podemos juntar as quatro operações em um único Program.cs. Assim, você executa o CRUD com EF Core de ponta a ponta com um só comando.

using EfCrud.Data;
using EfCrud.Entities;
using Microsoft.EntityFrameworkCore;

using var context = new AppDbContext();

// Create: insere uma nova categoria
var category = new Category
{
    Title = "Category 1",
    Slug = "category-1",
    Description = "Description of category 1"
};
context.Categories.Add(category);
await context.SaveChangesAsync();
Console.WriteLine($"Created category {category.Id}");

// Read: lista todas as categorias sem rastreamento
var categories = await context.Categories.AsNoTracking().ToListAsync();
foreach (var item in categories)
    Console.WriteLine($"{item.Id}: {item.Title}");

// Update: altera o título da categoria criada
category.Title = "Updated category";
await context.SaveChangesAsync();

// Delete: remove a categoria criada
context.Categories.Remove(category);
await context.SaveChangesAsync();
Console.WriteLine("Category removed");

Depois de salvar, o EF Core preenche o Id gerado pelo banco na própria instância. Por isso, conseguimos exibir o valor logo após o SaveChangesAsync.

Repare que, no update, nem chamamos o Update. Como a entidade já está rastreada pelo contexto, o EF Core detecta a mudança e gera o UPDATE sozinho.

Para executar, basta rodar dotnet run na pasta do projeto. Em seguida, confira no banco que a tabela Categories recebeu e perdeu o registro.

Conclusão

Neste artigo, montamos um CRUD com EF Core do zero: criamos o projeto, a entidade, o data context e a migração. Em seguida, executamos as quatro operações básicas com poucas linhas de código.

O EF Core abstrai o SQL e o banco, o que acelera bastante o desenvolvimento. Ainda assim, entender o que acontece por baixo, como no caso do IQueryable, evita problemas de performance.

Quer aprender mais sobre Entity Framework

FAQ

O que significa CRUD?

CRUD significa Create, Read, Update e Delete, ou seja, criar, ler, atualizar e excluir. São as quatro operações básicas de qualquer banco de dados.

Preciso escrever SQL para usar o EF Core?

Não. O EF Core traduz as consultas LINQ e as operações do contexto para SQL automaticamente, embora também permita executar SQL quando necessário.

Por que preciso chamar o SaveChangesAsync?

Porque o contexto guarda as alterações em memória. O SaveChangesAsync é o método que gera os comandos SQL e grava as mudanças no banco.

Qual a diferença entre Code First e Database First?

No Code First, você cria as classes e gera o banco a partir delas. No Database First, você parte de um banco existente e gera as classes a partir dele.

Posso usar SQLite em vez de SQL Server?

Sim. Basta instalar o pacote Microsoft.EntityFrameworkCore.Sqlite e trocar o UseSqlServer por UseSqlite no contexto.