Sobre esta aula
Na aula teórica começamos a responder a pergunta: "se meus dados estão no SQL Server, como eu trabalho com eles dentro de uma aplicação C#?". Vimos que mapeamento é a correspondência entre a estrutura dos dados no banco e sua representação no código, e que o Entity Framework Core é um ORM (Object-Relational Mapping) que faz essa ponte para nós: cuida da conexão, do mapeamento, das consultas, das inserções, alterações e exclusões, traduz as consultas para SQL e materializa os resultados em objetos.
O caminho que vimos foi: C# / Objetos → LINQ → EF Core → SQL → Banco de Dados — e depois o inverso: Banco → Resultado SQL → EF Core → Objetos C#. Nesta aula prática o objetivo é só um: se familiarizar com esse caminho, conectando em um banco real, fazendo as primeiras consultas com LINQ e vendo o SQL que o EF Core gera. Vamos aprofundar o assunto na próxima aula teórica.
O banco de dados da aula
O laboratório tem um servidor SQL Server compartilhado. Nele já existe o banco
AulaEFCore, com uma única tabela, a mesma que vimos na teoria
(com uma coluna a mais, Cargo):
Funcionarios
--------------------------------
Id int (chave primária, gerada pelo banco)
Nome nvarchar(100)
Cargo nvarchar(50)
Salario decimal(10,2)
Ativo bit
O servidor, o usuário e a senha de acesso serão informados pelo professor em aula.
O banco é o mesmo para a turma inteira. Tudo o que você inserir, alterar ou
apagar aparece para todos os colegas. Por isso, nos exemplos de inserção/alteração/exclusão, cada
um trabalha somente com os registros que criou, identificados pelo seu nome no campo
Nome. Nunca rode um Remove
ou alteração em massa sobre a tabela inteira.
Criando o Projeto no VS Code
- Abra o Visual Studio Code.
- Crie uma pasta com o seu nome em um local do computador de que você vá se lembrar depois (ex.: Área de Trabalho ou Documentos).
- No VS Code, vá em File > Open Folder e selecione a pasta criada.
- Abra o terminal integrado em Terminal > New Terminal.
- No terminal, execute um comando por vez, apertando Enter e esperando ele terminar antes de digitar o próximo.
Primeiro, crie o projeto:
dotnet new console -n AulaEFCore
Depois, entre na pasta do projeto que acabou de ser criada:
cd AulaEFCore
Agora adicione o pacote do EF Core para SQL Server (é ele que traz o EF Core e o "tradutor" de LINQ para o SQL do SQL Server):
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Deu erro de versão ao adicionar o pacote? Sem informar a versão, o comando
baixa a versão mais recente do EF Core, que pode exigir um .NET mais novo que o instalado na
máquina (erro NU1202, "not compatible with net8.0", por
exemplo). Veja a sua versão com dotnet --version e informe a
versão do pacote correspondente:
# .NET 8
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.11
# .NET 9
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
Por fim, rode o projeto para confirmar que tudo compila:
dotnet run
Passo 1 — O mapeamento: a classe Funcionario
Do lado do C#, cada linha da tabela Funcionarios vai virar um
objeto Funcionario. Crie um arquivo novo chamado
Funcionario.cs (na mesma pasta do
Program.cs) com o conteúdo:
public class Funcionario
{
public int Id { get; set; }
public string Nome { get; set; } = string.Empty;
public string Cargo { get; set; } = string.Empty;
public decimal Salario { get; set; }
public bool Ativo { get; set; }
}
Repare na correspondência, coluna por coluna: int ↔
int, nvarchar ↔
string, decimal ↔
decimal, bit ↔
bool. Por convenção, o EF Core entende sozinho que a
propriedade chamada Id é a chave primária.
O = string.Empty só serve para o compilador não reclamar
(com um aviso amarelo) de que a string poderia ficar nula. Não
muda nada no mapeamento.
Passo 2 — O DbContext e o DbSet
O DbContext é a principal classe de interação entre a aplicação
e o banco — uma espécie de sessão de trabalho com o banco. O
DbSet<Funcionario> é a representação, no código, da
tabela Funcionarios. Crie um arquivo chamado
AppDbContext.cs:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
public class AppDbContext : DbContext
{
public DbSet<Funcionario> Funcionarios { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(
"Server=SERVIDOR;Database=AulaEFCore;User Id=USUARIO;Password=SENHA;TrustServerCertificate=True");
// Descomente a linha abaixo para ver no console o SQL que o EF Core envia ao banco:
// options.LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information);
}
}
Troque SERVIDOR, USUARIO
e SENHA pelos dados informados em aula. Cada parte da
connection string corresponde a um campo da tela de conexão do SQL Server Management Studio:
Server é o "Nome do servidor", User Id e
Password são o logon e a senha da "Autenticação do SQL Server",
e Database é o banco que queremos abrir. O
TrustServerCertificate=True é necessário porque o servidor do
laboratório usa um certificado próprio, não emitido por uma autoridade certificadora — sem ele, a
conexão é recusada.
Repare que o nome do DbSet,
Funcionarios, é igual ao nome da tabela no banco. Novamente é
convenção: o EF Core usa o nome do DbSet como nome da
tabela, e o nome de cada propriedade como nome da coluna.
Exemplo 1 — A primeira consulta
Substitua todo o conteúdo do Program.cs por:
using Microsoft.EntityFrameworkCore;
using var db = new AppDbContext();
List<Funcionario> funcionarios = db.Funcionarios.ToList();
Console.WriteLine($"Total de funcionários: {funcionarios.Count}");
foreach (Funcionario f in funcionarios)
{
Console.WriteLine($"{f.Id,3} | {f.Nome,-20} | {f.Cargo,-14} | {f.Salario,10:N2} | {(f.Ativo ? "Ativo" : "Inativo")}");
}
Rode com dotnet run. Você não escreveu nenhuma linha de SQL, nem
leu nenhuma coluna "na mão": o EF Core montou o SELECT, executou
no SQL Server e transformou cada linha do resultado em um objeto
Funcionario — isso é a materialização.
Desafios:
- Descomente a linha do
LogTonoAppDbContext.cse rode de novo. Encontre no console oSELECTque o EF Core gerou. Ele é parecido com o que você escreveria? - Na classe
Funcionario, adicione uma propriedade que não existe no banco, por exemplopublic string Email { get; set; } = string.Empty;, e rode. Leia a mensagem de erro. O que ela diz sobre o mapeamento? Depois, remova a propriedade. - Agora remova temporariamente a propriedade
Cargoda classe (e doConsole.WriteLine) e rode. Dá erro? Olhe oSELECTgerado: a colunaCargoainda é consultada? Depois, volte a propriedade.
Pense e responda:
- Com base nos dois últimos desafios: quem "manda" no mapeamento — a tabela no banco ou a classe no C#? O que acontece quando os dois lados não batem?
Exemplo 2 — LINQ vira SQL
Agora a consulta que vimos na teoria. Com LINQ escrevemos a consulta em C#; o EF Core
interpreta essa expressão e a transforma em SQL. O método
ToQueryString() mostra esse SQL sem executar a consulta.
Substitua o Program.cs por:
using Microsoft.EntityFrameworkCore;
using var db = new AppDbContext();
var consulta = db.Funcionarios
.Where(f => f.Salario > 5000)
.Select(f => new
{
f.Nome,
f.Salario
});
Console.WriteLine("SQL gerado pelo EF Core:");
Console.WriteLine(consulta.ToQueryString());
Console.WriteLine();
var funcionarios = consulta.ToList(); // só aqui a consulta vai de fato ao banco
foreach (var f in funcionarios)
{
Console.WriteLine($"{f.Nome,-20} {f.Salario,10:N2}");
}
Desafios (para cada um, observe como o SQL gerado muda):
- Acrescente mais uma condição: apenas funcionários com salário acima de 5000 e ativos
(
&& f.Ativo). - Ordene o resultado do maior para o menor salário com
.OrderByDescending(f => f.Salario). Faz diferença colocar antes ou depois doSelect? - Inclua o
CargonoSelect. - Troque o
.ToList()por.Count()(sem oSelect) para descobrir quantos funcionários ganham mais de 5000. Qual SQL você imagina que é gerado? Confira com oLogTo. - Use
.Take(3)para trazer só os 3 maiores salários.
Pense e responda:
- Repare que
ToQueryString()foi chamado antes doToList()e mesmo assim já existia um SQL completo. O que isso indica sobre quando a consulta é realmente executada no banco? - No SQL gerado, o
WHEREaparece. Isso significa que o filtro é feito no banco ou no C#? Por que isso importa quando a tabela tem milhões de linhas?
Exemplo 3 — Inserindo com Add e SaveChanges
O DbContext também controla as alterações. O fluxo é:
Add() → EF Core registra a alteração →
SaveChanges() → EF Core gera SQL → SQL Server.
Deixe o LogTo ligado e troque SEU NOME
pelo seu nome de verdade:
using Microsoft.EntityFrameworkCore;
using var db = new AppDbContext();
var funcionario = new Funcionario
{
Nome = "SEU NOME",
Cargo = "Estagiário",
Salario = 2000,
Ativo = true
};
Console.WriteLine($"Id ANTES do SaveChanges: {funcionario.Id}");
db.Funcionarios.Add(funcionario);
Console.WriteLine("Add() chamado. Algum SQL apareceu no console até aqui?");
db.SaveChanges();
Console.WriteLine($"Id DEPOIS do SaveChanges: {funcionario.Id}");
Desafios:
- Rode e observe o log: o
INSERTaparece depois doAdd()ou só depois doSaveChanges()? - Anote o
Idimpresso depois doSaveChanges()— você vai precisar dele no próximo exemplo. Quem gerou esse número: o C# ou o banco? - Insira mais dois registros seus, desta vez chamando
Addduas vezes eSaveChangesuma única vez no final. Quantos comandos foram enviados ao banco? - Volte ao Exemplo 1 e confira que seus registros aparecem na listagem — junto com os dos colegas.
Exemplo 4 — Alterando e removendo
Para alterar, basta buscar o objeto, mudar a propriedade e chamar
SaveChanges(). Não existe um método "Update" nesse fluxo: o
DbContext percebe sozinho o que mudou. Troque
ID_DO_SEU_REGISTRO pelo Id
que você anotou no Exemplo 3:
using Microsoft.EntityFrameworkCore;
using var db = new AppDbContext();
int meuId = ID_DO_SEU_REGISTRO;
Funcionario? funcionario = db.Funcionarios.Find(meuId);
if (funcionario is null)
{
Console.WriteLine("Registro não encontrado.");
return;
}
Console.WriteLine($"Antes: {funcionario.Nome} - {funcionario.Salario:N2}");
funcionario.Salario = 6000;
db.SaveChanges();
Console.WriteLine($"Depois: {funcionario.Nome} - {funcionario.Salario:N2}");
E para remover um registro seu:
using Microsoft.EntityFrameworkCore;
using var db = new AppDbContext();
int meuId = ID_DO_SEU_REGISTRO;
Funcionario? funcionario = db.Funcionarios.Find(meuId);
if (funcionario is not null)
{
db.Funcionarios.Remove(funcionario);
db.SaveChanges();
Console.WriteLine("Registro removido.");
}
Confira o Id duas vezes antes de rodar a remoção. O banco é
compartilhado: um Id errado apaga o registro de um colega (ou
um dos dados iniciais da aula).
Desafios:
- Com o
LogToligado, observe oUPDATEgerado. Ele atualiza todas as colunas ou só oSalario? - Rode o código de alteração duas vezes seguidas com o mesmo valor (6000). Na segunda vez,
algum
UPDATEé enviado? Por quê?
Pense e responda:
- Se o
DbContextnão recebe nenhum aviso de queSalariomudou, como ele descobre o que precisa ir noUPDATE? (Guarde essa pergunta — voltaremos a ela na próxima aula teórica.)
Exercícios
Resolva cada item com LINQ sobre db.Funcionarios e, para cada
um, confira o SQL gerado (com ToQueryString() ou
LogTo). Os itens 1 a 5 são apenas consultas — não alteram
nada no banco.
- Liste nome e salário de todos os funcionários inativos.
- Liste os funcionários do cargo
"Desenvolvedor"em ordem alfabética de nome. - Mostre o maior, o menor e a média dos salários dos funcionários ativos
(dica:
Max,MineAverage). - Verifique se existe algum funcionário com salário acima de 15.000 (dica:
Any). Que tipo de valor esse método devolve? - Para cada cargo, mostre o nome do cargo, a quantidade de funcionários e a média salarial (dica:
GroupByseguido de umSelectcomg.Key,g.Count()eg.Average(f => f.Salario)). Veja se o SQL gerado usaGROUP BY. - Busque somente os seus registros (dica:
Wherecomf.Nome.Contains("SEU NOME")), aplique um aumento de 10% no salário de cada um e salve com um únicoSaveChanges(). - Ao final da aula, remova todos os seus registros do banco, deixando a tabela como estava.
No item 6, antes de salvar, imprima quantos registros a consulta encontrou. Se o número for maior
do que o que você inseriu, o filtro está pegando registros de outras pessoas — ajuste antes de
chamar SaveChanges().
Para refletir
Hoje você consultou, inseriu, alterou e removeu dados de um SQL Server sem escrever SQL. Isso não
quer dizer que o SQL sumiu: ele continua lá, só que gerado pelo EF Core a partir do LINQ e
do mapeamento entre a classe Funcionario e a tabela
Funcionarios. Por isso vale criar o hábito de olhar o SQL
gerado — é ele que diz se o filtro está sendo feito no banco, quantas colunas estão sendo
trazidas e quantos comandos estão sendo enviados. Na próxima aula teórica vamos aprofundar como o
DbContext acompanha as alterações, como configurar mapeamentos
que fogem das convenções e consultas LINQ mais avançadas.