Na aula anterior, entendemos por que precisamos de bancos de dados e como o Entity Framework Core faz a ponte entre os objetos do nosso programa e as tabelas do banco. Agora colocamos isso em prática: vamos conectar, pela primeira vez, um programa C# a um banco de dados real. Usaremos o PostgreSQL, um dos bancos de dados relacionais mais respeitados e usados do mundo — poderoso, gratuito e de código aberto. Ao final desta aula, você terá um programa que salva e busca informações num banco de dados de verdade, usando o LINQ que já conhece. Como esta é a primeira vez que instalamos e configuramos um banco, iremos com cuidado, passo a passo, incluindo a instalação no WSL, como sempre. É uma aula prática e densa, mas cada passo se apoia em coisas que você já domina.
Passo 1: instalar o PostgreSQL
Antes de qualquer código, precisamos de um banco de dados rodando na máquina. No WSL/Ubuntu (o ambiente que recomendamos desde o início do curso), a instalação é feita com alguns comandos no terminal:
sudo apt-get update
sudo apt-get install -y postgresql
# Inicia o serviço do banco de dados:
sudo service postgresql start
Com o PostgreSQL instalado e o serviço iniciado, precisamos criar um usuário e um banco de dados para o nosso projeto. Entramos no console do PostgreSQL como o administrador e criamos ambos:
sudo -u postgres psql # entra no console do PostgreSQL
Dentro desse console (você notará que o sinal de prompt muda), digite os comandos abaixo, um por vez, e depois saia com \q:
CREATE USER curso WITH PASSWORD 'senha123';
CREATE DATABASE loja_db OWNER curso;
\q
Esses comandos criam um usuário chamado curso (com uma senha) e um banco de dados chamado loja_db, do qual esse usuário é dono. (No Windows, alternativamente, você pode baixar o instalador do PostgreSQL em postgresql.org, que inclui uma interface gráfica; o restante da aula é idêntico.) Com o banco de pé, voltamos ao conforto do C#.
Passo 2: criar o projeto e adicionar o EF Core
Criamos um projeto de console e adicionamos, com o NuGet (que você aprendeu na Aula #44), as bibliotecas necessárias: o EF Core e o provedor específico do PostgreSQL — a biblioteca que ensina o EF Core a conversar com esse banco em particular:
dotnet new console -n LojaDb
cd LojaDb
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
Guarde o conceito de provedor: é a biblioteca que adapta o EF Core a um banco de dados específico. O Npgsql...PostgreSQL é o provedor do PostgreSQL. Na próxima aula, veremos que trocar de banco é, em boa parte, trocar o provedor — uma demonstração do valor da abstração.
Passo 3: definir a classe e o contexto
Agora, o C#. Definimos a classe que representa o que queremos guardar — um produto, digamos. É uma classe comum, com properties, exatamente como você aprendeu; a única novidade é uma property Id, que o EF Core reconhece automaticamente como o identificador único de cada registro (a "chave" que distingue cada linha na tabela):
class Produto
{
public int Id { get; set; } // identificador único (o EF Core reconhece 'Id')
public string Nome { get; set; } = "";
public decimal Preco { get; set; }
}
Em seguida, criamos a peça central do EF Core: o contexto, uma classe que representa a conexão com o banco e expõe as tabelas como coleções. Ela herda de uma classe do EF Core (DbContext) e declara cada tabela como uma property do tipo DbSet:
using Microsoft.EntityFrameworkCore;
class LojaContext : DbContext
{
// Cada DbSet corresponde a uma TABELA no banco. Esta é a tabela de Produtos.
public DbSet<Produto> Produtos => Set<Produto>();
// Configura QUAL banco usar e COMO se conectar a ele:
protected override void OnConfiguring(DbContextOptionsBuilder opcoes)
{
opcoes.UseNpgsql(
"Host=localhost;Database=loja_db;Username=curso;Password=senha123");
}
}
Duas peças a entender. O DbSet<Produto> Produtos declara que existe uma tabela de produtos — é sobre ela que faremos as consultas LINQ. E o UseNpgsql(...), com aquela string de conexão, diz ao EF Core para usar o provedor PostgreSQL e como se conectar (o endereço do banco, o nome do banco, o usuário e a senha que criamos). Note que o contexto é um recurso que se conecta ao banco; por isso, será usado com o using que garante a liberação de recursos — como veremos no código a seguir.
Passo 4: criar a tabela a partir da classe
Nossa classe Produto descreve o que queremos guardar, mas o banco ainda está vazio — a tabela de produtos ainda não existe nele. O EF Core cria as tabelas a partir das nossas classes através de um mecanismo chamado migração (migration). Primeiro, instalamos a ferramenta de linha de comando do EF Core (uma única vez), e então geramos e aplicamos a migração:
# Instala a ferramenta do EF Core (uma vez só, para toda a máquina):
dotnet tool install --global dotnet-ef
# Gera a migração: compara as classes com o banco e descreve o que criar:
dotnet ef migrations add Inicial
# Aplica a migração: cria de fato a tabela no PostgreSQL:
dotnet ef database update
O migrations add gera um código que descreve as mudanças necessárias (criar a tabela de produtos com suas colunas); o database update executa esse código contra o banco, criando a tabela de verdade. A beleza das migrações é que, quando você mudar as classes depois (adicionar uma property, por exemplo), gera uma nova migração e o EF Core sabe atualizar o banco sem perder os dados existentes — é o controle de versão da estrutura do seu banco.
Passo 5: salvar e buscar dados
Chegou o momento que dá sentido a toda a preparação. Salvar objetos no banco é adicioná-los ao DbSet e mandar salvar. Note que operações de banco são operações de espera (conversamos com um sistema externo), então usamos a assincronia (async/await) que você conheceu na Aula #48:
using var db = new LojaContext(); // abre a conexão (liberada ao fim pelo 'using')
// INSERIR: adiciona objetos e salva no banco.
db.Produtos.Add(new Produto { Nome = "Caderno", Preco = 12.5m });
db.Produtos.Add(new Produto { Nome = "Caneta", Preco = 2m });
await db.SaveChangesAsync(); // AQUI os dados são de fato gravados no banco
// BUSCAR: usando o MESMO LINQ que você já conhece!
var baratos = await db.Produtos
.Where(p => p.Preco < 10) // vira uma busca no banco (não na memória!)
.OrderBy(p => p.Nome)
.ToListAsync(); // executa a consulta no banco
foreach (var p in baratos)
Console.WriteLine($"{p.Id}: {p.Nome} - R$ {p.Preco}");
Observe atentamente o que aconteceu, pois é a recompensa da fase. Para salvar, adicionamos objetos ao DbSet e chamamos SaveChangesAsync, e o EF Core os transforma em linhas na tabela. Para buscar, usamos Where e OrderBy — LINQ puro, idêntico ao que você usava em listas — mas o EF Core não trouxe todos os produtos para a memória e filtrou aqui; ele traduziu seu LINQ numa busca que o banco executou, trazendo só os resultados. É o mesmo LINQ, agora com o poder de um banco de dados por baixo. Atualizar e remover seguem a mesma lógica: você busca o objeto, altera suas properties (ou o remove do DbSet), e chama SaveChangesAsync, e o EF Core cuida de refletir isso no banco:
var produto = await db.Produtos.FirstOrDefaultAsync(p => p.Nome == "Caneta");
if (produto != null) // tratamento de nulo (Aula #41)
{
produto.Preco = 3m; // o EF Core RASTREIA essa mudança
await db.SaveChangesAsync(); // e a grava no banco automaticamente
}
O EF Core rastreia os objetos que você buscou: ao alterar uma property e salvar, ele detecta a mudança e atualiza a linha correspondente no banco, sem que você escreva nenhum comando de banco. Você trabalha com objetos; o banco reflete suas ações.
A persistência de verdade entrou em funcionamento. O PostgreSQL foi instalado e configurado, o EF Core foi adicionado ao projeto pelo NuGet, as classes do programa passaram a descrever as tabelas, e uma migração gerou a estrutura do banco a partir delas.
O detalhe que costuma surpreender é que salvar e buscar dados se pareceu muito com manipular uma coleção em memória: adicionar um objeto, consultar com os mesmos operadores já conhecidos, confirmar as alterações. Foi o ORM traduzindo tudo em comandos de banco nos bastidores. Reiniciar a aplicação e encontrar os dados intactos é a demonstração concreta do que se ganhou.
Fontes e leituras recomendadas
- Entity Framework Core (documentação) — a referência oficial do ORM: contexto, DbSet, consultas e migrações; a base desta aula.
- Provedor Npgsql para EF Core — o provedor de PostgreSQL, com detalhes de conexão.
- Migrações no EF Core — como criar e aplicar migrações para gerar e evoluir as tabelas.
- Consultando dados com EF Core — como o LINQ é traduzido em buscas no banco.
- Instalação do PostgreSQL — os instaladores oficiais por sistema; para o WSL, siga o guia de Linux/Ubuntu.
Exercícios
Exercício 1
Instale o PostgreSQL (no WSL ou no Windows), crie o usuário curso e o banco loja_db, e confirme que consegue entrar no console com psql. Anote a string de conexão que você usará no C#.
Ver resposta
✓ Resposta: Após instalar e iniciar o serviço, sudo -u postgres psql abre o console; os comandos CREATE USER curso WITH PASSWORD 'senha123'; e CREATE DATABASE loja_db OWNER curso; criam o usuário e o banco. A string de conexão correspondente é Host=localhost;Database=loja_db;Username=curso;Password=senha123. Conseguir entrar no console confirma que o serviço está no ar e as credenciais funcionam.
Exercício 2
Crie o projeto LojaDb, adicione as bibliotecas do EF Core e do Npgsql, escreva a classe Produto e o LojaContext, e gere e aplique a primeira migração. Confirme, no console do PostgreSQL, que a tabela de produtos foi criada.
Ver resposta
✓ Resposta: Após criar o projeto, adicionar Microsoft.EntityFrameworkCore.Design e Npgsql.EntityFrameworkCore.PostgreSQL, escrever a classe Produto e o LojaContext, os comandos dotnet ef migrations add Inicial e dotnet ef database update criam a tabela. No console do PostgreSQL, o comando \dt lista as tabelas do banco e deve mostrar a tabela de produtos, confirmando que a estrutura foi criada a partir da classe.
Exercício 3
Escreva um programa que insira três produtos, salve-os com SaveChangesAsync, e depois busque e exiba todos os produtos com preço abaixo de um valor, ordenados por nome, usando LINQ. Confirme os resultados.
Ver resposta
✓ Resposta: Exemplo:
using var db = new LojaContext();
db.Produtos.Add(new Produto { Nome = "Lápis", Preco = 1.5m });
db.Produtos.Add(new Produto { Nome = "Régua", Preco = 4m });
db.Produtos.Add(new Produto { Nome = "Mochila", Preco = 80m });
await db.SaveChangesAsync();
var baratos = await db.Produtos.Where(p => p.Preco < 10).OrderBy(p => p.Nome).ToListAsync();
foreach (var p in baratos) Console.WriteLine($"{p.Nome}: R$ {p.Preco}");
// Exibe Lápis e Régua (Mochila fica de fora por custar 80), em ordem alfabética.
Exercício 4
Adicione uma nova property à classe Produto (por exemplo, Estoque, um inteiro), gere uma segunda migração e aplique-a. Explique, com base na aula, o que o EF Core fez ao banco sem que você perdesse os produtos já inseridos.
Ver resposta
✓ Resposta: Após adicionar public int Estoque { get; set; } à classe Produto, dotnet ef migrations add AdicionaEstoque gera uma migração descrevendo a mudança, e dotnet ef database update a aplica. O EF Core executou um comando que adicionou a nova coluna (Estoque) à tabela existente, preenchendo as linhas já presentes com o valor padrão (0, para inteiros), sem apagar nem recriar a tabela. Por isso os produtos já inseridos foram preservados: as migrações evoluem a estrutura do banco de forma incremental, adicionando o que mudou em vez de reconstruir tudo, o que mantém os dados existentes intactos.
Exercício 5
Sem código: explique o que significa dizer que, ao escrever db.Produtos.Where(p => p.Preco < 10), o filtro acontece "no banco, não na memória". Por que isso é mais eficiente do que trazer todos os produtos para o programa e filtrá-los com LINQ numa lista?
Ver resposta
✓ Resposta: Dizer que o filtro acontece "no banco, não na memória" significa que o EF Core não traz todos os produtos do banco para dentro do programa para então filtrá-los ali; em vez disso, ele traduz o Where(p => p.Preco < 10) numa instrução de busca que o banco de dados executa, e o banco devolve ao programa apenas os produtos que já satisfazem a condição. Isso é mais eficiente porque, com muitos registros, trazer todos eles para a memória do programa só para descartar a maioria seria um enorme desperdício — de tempo (transferir dados desnecessários pela conexão), de memória (carregar tudo) e de processamento. Deixar o banco fazer a filtragem aproveita que ele é especializado e otimizado para isso, e faz trafegar apenas o resultado relevante, tornando a operação muito mais rápida e econômica, especialmente em escala.