Separate concerns into API, Application, Domain, Infrastructure projects.
// Domain/Entities/Todo.cs
public sealed record Todo(Guid Id, string Title, bool Done);
// Domain/Abstractions/ITodoRepository.cs
public interface ITodoRepository { Task<IReadOnlyList<Todo>> ListAsync(CancellationToken ct); }
// Application/Todos/Queries/ListTodos.cs
public sealed record ListTodosQuery() : IRequest<IReadOnlyList<Todo>>;
public sealed class ListTodosHandler(ITodoRepository repo) : IRequestHandler<ListTodosQuery, IReadOnlyList<Todo>>
{
public Task<IReadOnlyList<Todo>> Handle(ListTodosQuery request, CancellationToken ct) => repo.ListAsync(ct);
}
// Infrastructure/Data/TodoRepository.cs
public sealed class TodoRepository(AppDbContext db) : ITodoRepository
{
public async Task<IReadOnlyList<Todo>> ListAsync(CancellationToken ct) =>
await db.Todos.AsNoTracking().Select(e => new Todo(e.Id, e.Title, e.Done)).ToListAsync(ct);
}
// API/Program.cs
builder.Services.AddDbContext<AppDbContext>(...);
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ListTodosHandler).Assembly));
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
Minimal four-project setup: Domain, Application (with MediatR), Infrastructure (EF Core), and API.
// Domain/Entities/Todo.cs
namespace Domain.Entities;
public sealed record Todo(Guid Id, string Title, bool Done);
// Domain/Abstractions/ITodoRepository.cs
namespace Domain.Abstractions;
public interface ITodoRepository { Task<IReadOnlyList<Todo>> ListAsync(CancellationToken ct); }
// Application/Todos/Queries/ListTodos.cs
using Domain.Abstractions;
using Domain.Entities;
using MediatR;
public sealed record ListTodosQuery() : IRequest<IReadOnlyList<Todo>>;
public sealed class ListTodosHandler(ITodoRepository repo)
: IRequestHandler<ListTodosQuery, IReadOnlyList<Todo>>
{
public Task<IReadOnlyList<Todo>> Handle(ListTodosQuery request, CancellationToken ct)
=> repo.ListAsync(ct);
}
// Infrastructure/Data/AppDbContext.cs
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Todo> Todos => Set<Todo>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Todo>().HasKey(t => t.Id);
base.OnModelCreating(modelBuilder);
}
}
// Infrastructure/Data/TodoRepository.cs
using Domain.Abstractions;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
public sealed class TodoRepository(AppDbContext db) : ITodoRepository
{
public async Task<IReadOnlyList<Todo>> ListAsync(CancellationToken ct)
=> await db.Todos.AsNoTracking().ToListAsync(ct);
}
// API/Program.cs
using MediatR;
using Microsoft.EntityFrameworkCore;
using Domain.Abstractions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseInMemoryDatabase("app"));
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(ListTodosHandler).Assembly));
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
app.MapGet("/todos", async (IMediator mediator, CancellationToken ct)
=> await mediator.Send(new ListTodosQuery(), ct));
app.Run();
Notes
UseDbContext
as needed. The handler remains unchanged.Paste into a Minimal API solution (see sandboxes/dotnet-minimal-api):