Projeto: CashFlow API · Stack: .NET 10 · C# · MySQL · Entity Framework Core
Repositório: martoxm/cashflow-api
| # | Commit | Conceito |
|---|---|---|
| 1 | Add Expense entity and PaymentType enum |
Mapeamento de entidades |
| 2 | Add CashFlowDbContext with MySQL configuration |
DbContext + MySQL |
| 3 | Introduce DI and repository pattern |
Injeção de Dependência |
| 4 | Refactor ExpensesRepository for DI |
DI no repositório |
| 5 | Refactor DB config to use connection string |
Configuração via appsettings |
| 6 | Introduce Unit of Work pattern |
Unit of Work |
| 7 | Make expense registration fully async |
async/await |
| 8 | Add AutoMapper integration |
AutoMapper |
| 9 | Add GetAllExpenses endpoint |
Consulta + AutoMapper |
| 10 | Use AsNoTracking in GetAll |
Performance com EF Core |
| 11 | Add GetExpenseById |
Consulta por ID |
| 12 | Handle not found errors |
NotFoundException |
| 13 | Refactor repository into read/write interfaces |
ISP (SOLID) |
| 14 | Refactor exception handling to CashFlowException |
Hierarquia de exceções |
| 15 | Add expense deletion |
DELETE endpoint |
| 16 | Refactor DTOs and add update endpoint |
PUT endpoint + DTO unificado |
O EF Core usa classes C# como entidades para representar tabelas do banco de dados. O mapeamento é feito por convenção (nomes de propriedades) ou configuração explícita via Fluent API.
352eabf — Add Expense entity and PaymentType enum
// CashFlow.Domain/Entities/Expense.cs
public class Expense
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public PaymentType PaymentType { get; set; }
}// CashFlow.Domain/Enums/PaymentType.cs
public enum PaymentType
{
Cash = 1,
CreditCard,
DebitCard,
EletronicTransfer
}long Id→ convenção do EF Core para chave primária (gerada automaticamente)- Propriedades
string?= nullable → coluna aceita NULL no banco enumé mapeado comointpor padrão na coluna
O DbContext é a ponte entre sua aplicação e o banco de dados. Ele gerencia conexões, rastreia entidades e executa queries SQL através do LINQ.
bbbe349 — Add CashFlowDbContext with MySQL configuration
// CashFlow.Infrastructure/DataAccess/CashFlowDbContext.cs
internal class CashFlowDbContext : DbContext
{
public DbSet<Expense> Expenses { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql(
"Server=localhost;Database=cashflow;Uid=root;Pwd=root;",
ServerVersion.AutoDetect("Server=localhost;...")
);
}
}Isso foi refatorado no próximo commit — nunca exponha credenciais no código!
ce7d344 — Refactor DB config to use connection string from settings
// appsettings.Development.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=cashflow;Uid=root;Pwd=root;"
}
}// CashFlowDbContext.cs — agora via construtor (DI)
internal class CashFlowDbContext : DbContext
{
public CashFlowDbContext(DbContextOptions<CashFlowDbContext> options) : base(options) { }
public DbSet<Expense> Expenses { get; set; }
}// DependencyInjectionExtension.cs
public static void AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
var connectionString = config.GetConnectionString("DefaultConnection");
var serverVersion = ServerVersion.AutoDetect(connectionString);
services.AddDbContext<CashFlowDbContext>(opts =>
opts.UseMySql(connectionString, serverVersion));
}DbSet<T>representa a tabela no bancoDbContextOptionspermite injetar configurações de fora (DI-friendly)UseMySql+Pomelo.EntityFrameworkCore.MySql= integração com MySQLinternal→ encapsula o DbContext na camada de Infrastructure
DI é um padrão onde as dependências de uma classe são fornecidas de fora, não instanciadas dentro dela. O .NET tem um container de DI nativo em IServiceCollection.
6c2e3bb — Introduce Dependency Injection and repository pattern
public class ExpensesController : ControllerBase
{
[HttpPost]
public IActionResult Register(RequestRegisterExpenseJson request)
{
var useCase = new RegisterExpenseUseCase(); // ❌ dependência hardcoded
useCase.Execute(request);
...
}
}public class ExpensesController : ControllerBase
{
private readonly IRegisterExpenseUseCase _useCase;
public ExpensesController(IRegisterExpenseUseCase useCase) // ✅ injetado
{
_useCase = useCase;
}
}// DependencyInjectionExtension.cs
public static void AddApplication(this IServiceCollection services)
{
services.AddScoped<IRegisterExpenseUseCase, RegisterExpenseUseCase>();
}
public static void AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<IExpensesRepository, ExpensesRepository>();
// ... DbContext config
}// Program.cs
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);| Lifetime | Descrição | Quando usar |
|---|---|---|
Transient |
Nova instância a cada injeção | Serviços leves, sem estado |
Scoped |
Uma instância por request HTTP | Use Cases, Repositories, DbContext |
Singleton |
Uma instância para toda a app | Configurações, cache global |
💡 Regra geral: Use
Scopedpara DbContext e tudo que depende dele.
O Unit of Work centraliza o SaveChanges() — separa a responsabilidade de persistir mudanças dos repositórios, que só adicionam/removem entidades no contexto.
712bfa5 — Introduce Unit of Work pattern
// IUnitOfWork.cs
public interface IUnitOfWork
{
Task Commit();
}
// UnitOfWork.cs
internal class UnitOfWork : IUnitOfWork
{
private readonly CashFlowDbContext _dbContext;
public UnitOfWork(CashFlowDbContext dbContext) => _dbContext = dbContext;
public async Task Commit() => await _dbContext.SaveChangesAsync();
}// RegisterExpenseUseCase.cs
public class RegisterExpenseUseCase : IRegisterExpenseUseCase
{
private readonly IExpensesRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public async Task<ResponseRegisterExpenseJson> Execute(RequestExpenseJson request)
{
// validações...
var expense = _mapper.Map<Expense>(request);
await _repository.Add(expense);
await _unitOfWork.Commit(); // ✅ SaveChanges centralizado aqui
return _mapper.Map<ResponseRegisterExpenseJson>(expense);
}
}- Repositório só conhece "o quê" salvar
- Unit of Work decide "quando" salvar (fim da operação)
- Facilita transações (vários repositórios, um único commit)
async/await libera a thread enquanto espera operações de I/O (banco de dados, rede), melhorando a escalabilidade da API.
e787a19 — Make expense registration fully async
public ResponseRegisterExpenseJson Execute(RequestExpenseJson request)
{
_repository.Add(expense);
_unitOfWork.Commit();
return response;
}// Interface
public interface IRegisterExpenseUseCase
{
Task<ResponseRegisterExpenseJson> Execute(RequestExpenseJson request);
}
// Use Case
public async Task<ResponseRegisterExpenseJson> Execute(RequestExpenseJson request)
{
await _repository.Add(expense);
await _unitOfWork.Commit();
return response;
}
// Repository
public async Task Add(Expense expense)
{
await _dbContext.Expenses.AddAsync(expense);
}
// Unit of Work
public async Task Commit()
{
await _dbContext.SaveChangesAsync();
}
// Controller
[HttpPost]
[ProducesResponseType(typeof(ResponseRegisterExpenseJson), StatusCodes.Status201Created)]
public async Task<IActionResult> Register(RequestExpenseJson request)
{
var response = await _useCase.Execute(request);
return Created(string.Empty, response);
}- Método
asyncdeve terawaitdentro, ou compilador avisa Task= void assíncrono;Task<T>= retorno assíncrono- Sempre use métodos
*Asyncdo EF Core:AddAsync,SaveChangesAsync,ToListAsync - Não misture
.Resultou.Wait()comawait→ deadlock!
O AutoMapper elimina código repetitivo de mapeamento entre objetos (ex: Expense → ResponseExpenseJson).
ea94460— Add AutoMapper integration and update DI4526154— Add AutoMapper integration and improve API responses
<!-- CashFlow.Application.csproj -->
<PackageReference Include="AutoMapper" Version="13.0.1" />// AutoMapper/ExpenseProfile.cs
public class ExpenseProfile : Profile
{
public ExpenseProfile()
{
// Request → Entity
CreateMap<RequestExpenseJson, Expense>();
// Entity → Response
CreateMap<Expense, ResponseRegisterExpenseJson>();
CreateMap<Expense, ResponseExpenseJson>();
CreateMap<Expense, ResponseShortExpenseJson>();
}
}// DependencyInjectionExtension.cs
public static void AddApplication(this IServiceCollection services)
{
services.AddAutoMapper(typeof(ExpenseProfile).Assembly); // ✅ sem pacote extra
}public class RegisterExpenseUseCase : IRegisterExpenseUseCase
{
private readonly IMapper _mapper;
public async Task<ResponseRegisterExpenseJson> Execute(RequestExpenseJson request)
{
var expense = _mapper.Map<Expense>(request); // DTO → Entity
await _repository.Add(expense);
await _unitOfWork.Commit();
return _mapper.Map<ResponseRegisterExpenseJson>(expense); // Entity → DTO
}
}Profile= classe que define os mapeamentosCreateMap<Source, Destination>()= configura mapeamento bidirecional ou unidirecional- Por padrão, mapeia propriedades de mesmo nome
- Para nomes diferentes:
.ForMember(dest => dest.X, opt => opt.MapFrom(src => src.Y))
Por padrão, o EF Core rastreia todas as entidades retornadas de queries — armazenando o estado original para detectar mudanças no SaveChanges. Isso tem custo de memória e CPU.
7b1288a — Use AsNoTracking in GetAll for better performance
// ExpensesRepository.cs — SEM AsNoTracking (padrão)
public async Task<List<Expense>> GetAll()
{
return await _dbContext.Expenses.ToListAsync(); // EF rastreia cada entidade
}
// COM AsNoTracking ✅
public async Task<List<Expense>> GetAll()
{
return await _dbContext.Expenses
.AsNoTracking()
.ToListAsync();
}| Situação | AsNoTracking? |
|---|---|
| Leitura pura (GET, listagem) | ✅ Sempre |
| Vai atualizar/deletar depois | ❌ Não usar |
| Operações de escrita | ❌ Não usar |
💡 Regra: Se vai apenas ler e retornar dados, use
AsNoTracking()— melhora performance em 20–40% em consultas grandes.
96fd0c2 — Refactor expense repository into read/write interfaces
"Uma classe não deve ser forçada a depender de interfaces que não usa."
public interface IExpensesRepository
{
Task Add(Expense expense);
Task<List<Expense>> GetAll();
Task<Expense?> GetById(long id);
Task Delete(long id);
Task Update(Expense expense);
}// IExpensesReadOnlyRepository.cs
public interface IExpensesReadOnlyRepository
{
Task<List<Expense>> GetAll();
Task<Expense?> GetById(long id);
}
// IExpensesWriteOnlyRepository.cs
public interface IExpensesWriteOnlyRepository
{
Task Add(Expense expense);
Task Delete(long id);
}
// IExpensesUpdateOnlyRepository.cs
public interface IExpensesUpdateOnlyRepository
{
Task<Expense?> GetById(long id); // tracking habilitado para update
Task Update(Expense expense);
}// ExpensesRepository.cs — implementa as três
internal class ExpensesRepository :
IExpensesReadOnlyRepository,
IExpensesWriteOnlyRepository,
IExpensesUpdateOnlyRepository
{
// implementações...
}// GetAllExpenseUseCase — só lê
public class GetAllExpenseUseCase
{
private readonly IExpensesReadOnlyRepository _repository; // ✅ sem acesso a escrita
}
// DeleteExpenseUseCase — só escreve
public class DeleteExpenseUseCase
{
private readonly IExpensesWriteOnlyRepository _repository; // ✅ sem acesso a leitura
}| Princípio | Aplicação no CashFlow |
|---|---|
| S — Single Responsibility | Cada Use Case tem uma responsabilidade (RegisterExpense, GetAllExpenses, DeleteExpense) |
| O — Open/Closed | Novos endpoints = novos Use Cases, sem modificar os existentes |
| L — Liskov Substitution | ExpensesRepository substitui qualquer interface que implementa |
| I — Interface Segregation | IExpensesReadOnlyRepository / IExpensesWriteOnlyRepository |
| D — Dependency Inversion | Controllers dependem de interfaces (IRegisterExpenseUseCase), não implementações |
7731a30 — Refactor exception handling to use CashFlowException base
// CashFlowException.cs — classe base abstrata
public abstract class CashFlowException : SystemException
{
protected CashFlowException(string message) : base(message) { }
public abstract int StatusCode { get; }
public abstract IList<string> GetErrors();
}
// ErrorOnValidationException.cs
public class ErrorOnValidationException : CashFlowException
{
private readonly IList<string> _errors;
public ErrorOnValidationException(IList<string> errors) : base(string.Empty)
=> _errors = errors;
public override int StatusCode => StatusCodes.Status400BadRequest;
public override IList<string> GetErrors() => _errors;
}
// NotFoundException.cs
public class NotFoundException : CashFlowException
{
public NotFoundException(string message) : base(message) { }
public override int StatusCode => StatusCodes.Status404NotFound;
public override IList<string> GetErrors() => [Message];
}// ExceptionFilter.cs — centralizado
public class ExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is CashFlowException ex)
{
context.HttpContext.Response.StatusCode = ex.StatusCode;
context.Result = new ObjectResult(new ResponseErrorJson(ex.GetErrors()));
}
// fallback para erro 500...
}
}ExceptionFiltertrata qualquerCashFlowExceptioncom um únicoif- Adicionar novo tipo de erro = criar nova classe filha, sem mexer no Filter (OCP)
- Cada exceção define seu próprio
StatusCodeeGetErrors()
Request HTTP
↓
ExpensesController.GetById(long id)
↓
IGetExpenseByIdUseCase.Execute(id)
↓
IExpensesReadOnlyRepository.GetById(id) ← AsNoTracking
↓
CashFlowDbContext → MySQL Query
↓
Expense entity → AutoMapper → ResponseExpenseJson
↓
200 OK com DTO (ou 404 via NotFoundException)
| Método | Rota | Use Case | Repository Interface |
|---|---|---|---|
POST |
/expenses |
RegisterExpenseUseCase |
IWriteOnly + IUnitOfWork |
GET |
/expenses |
GetAllExpenseUseCase |
IReadOnly + AsNoTracking |
GET |
/expenses/{id} |
GetExpenseByIdUseCase |
IReadOnly + AsNoTracking |
PUT |
/expenses/{id} |
UpdateExpenseUseCase |
IUpdateOnly (com tracking) |
DELETE |
/expenses/{id} |
DeleteExpenseUseCase |
IWriteOnly + IUnitOfWork |
<!-- CashFlow.Infrastructure.csproj -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="10.0.0" />
<!-- CashFlow.Application.csproj -->
<PackageReference Include="AutoMapper" Version="13.0.1" />- Qual a diferença entre
Scoped,TransienteSingletonno DI? - Por que o
DbContextdeve serScopede nãoSingleton? - Quando não usar
AsNoTracking? - O que acontece se você chamar
SaveChangesdentro do repositório em vez do Unit of Work? - Como o
AutoMappersabe como mapear propriedades com nomes diferentes? - Qual princípio SOLID justifica a separação de
IExpensesReadOnlyRepositoryeIExpensesWriteOnlyRepository? - Por que a classe
CashFlowExceptionéabstract? - O que é o
DbSet<T>e para que serve?
Gerado com base nos commits do repositório martoxm/cashflow-api · Módulo 2 · Mai 2026