Capstone 2: build TaskFlow
Implement the TaskFlow API step by step — solution setup, EF Core with PostgreSQL, endpoints, validation, authentication, background email and tests.
40 min read
You will learn
- Set up a multi-project solution with PostgreSQL in Docker
- Implement entities, migrations and CRUD endpoints with validation
- Secure the API with JWT authentication and membership-based authorization
- Send notifications in the background and cover the API with tests
How to use this lesson
This is a build guide, not copy-paste code. Each milestone lists what to build, pointers to the lessons that cover it, and the essential snippets. Commit after every milestone, and keep dotnet test green.
Milestone 1 — Solution setup
mkdir TaskFlow && cd TaskFlow
git init
dotnet new gitignore
dotnet new sln -n TaskFlow
dotnet new web -o src/TaskFlow.Api
dotnet new xunit -o tests/TaskFlow.Api.Tests
dotnet sln add src/TaskFlow.Api tests/TaskFlow.Api.Tests
dotnet add tests/TaskFlow.Api.Tests reference src/TaskFlow.Api
cd src/TaskFlow.Api
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerStart PostgreSQL for development:
docker run -d --name taskflow-db -e POSTGRES_PASSWORD=dev-only -e POSTGRES_DB=taskflow -p 5432:5432 postgres:16
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:TaskFlow" "Host=localhost;Database=taskflow;Username=postgres;Password=dev-only"Covered in: The dotnet CLI · NuGet · Configuration · Docker.
Milestone 2 — Data model and migrations
Create the entities from your design and a DbContext:
public class TaskFlowDb(DbContextOptions<TaskFlowDb> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<ProjectMember> Members => Set<ProjectMember>();
public DbSet<TaskItem> Tasks => Set<TaskItem>();
public DbSet<Comment> Comments => Set<Comment>();
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<User>().HasIndex(u => u.Email).IsUnique();
b.Entity<ProjectMember>().HasKey(m => new { m.ProjectId, m.UserId });
b.Entity<TaskItem>().HasIndex(t => new { t.ProjectId, t.Status });
b.Entity<TaskItem>().HasIndex(t => new { t.AssigneeId, t.Status, t.DueDate });
b.Entity<TaskItem>().Property(t => t.Title).HasMaxLength(200);
b.Entity<TaskItem>().Property(t => t.Status).HasConversion<string>();
}
}builder.Services.AddDbContext<TaskFlowDb>(o => o.UseNpgsql(builder.Configuration.GetConnectionString("TaskFlow")));dotnet ef migrations add InitialCreate
dotnet ef database updateCovered in: EF Core basics · Relationships · Indexes and transactions.
Milestone 3 — Endpoints
Implement the API from your design, one feature at a time (projects → tasks → comments → "my tasks"). Organize with route groups and feature files:
public static class TaskEndpoints
{
public static void MapTaskEndpoints(this IEndpointRouteBuilder app)
{
var tasks = app.MapGroup("/projects/{projectId:int}/tasks").RequireAuthorization();
tasks.MapGet("/", async (int projectId, TaskStatus? status, int limit, int? after,
TaskFlowDb db, CancellationToken ct) =>
{
limit = Math.Clamp(limit == 0 ? 20 : limit, 1, 100);
var items = await db.Tasks.AsNoTracking()
.Where(t => t.ProjectId == projectId && (status == null || t.Status == status))
.Where(t => after == null || t.Id > after)
.OrderBy(t => t.Id)
.Take(limit)
.Select(t => new TaskDto(t.Id, t.Title, t.Status, t.DueDate, t.AssigneeId))
.ToListAsync(ct);
return TypedResults.Ok(items);
});
// POST, PATCH, DELETE …
}
}Requirements for every endpoint:
- DTOs in and out — never expose entities directly.
- Validation (title required, ≤ 200 chars, due date not in the past…) →
400with Problem Details. - Correct status codes:
201+Locationon create,204on delete,404for unknown ids. CancellationTokenpassed to EF Core.
Covered in: Designing REST APIs · Minimal APIs · Controllers (validation) · Cancellation.
Milestone 4 — Authentication and authorization
- Add JWT bearer authentication. For development, issue tokens with
dotnet user-jwts create— or implement/auth/registerand/auth/loginwith ASP.NET Core Identity's password hasher and your own token issuing (keep the signing key in user secrets). - Require authentication on everything except register/login.
- Enforce membership: a user may only access projects they're a member of. Return
404(not403) for projects they can't see, so ids can't be probed. - Only the owner may delete a project or remove members — a policy or a resource-based check.
static async Task<bool> IsMemberAsync(TaskFlowDb db, int projectId, ClaimsPrincipal user, CancellationToken ct) =>
await db.Members.AnyAsync(m => m.ProjectId == projectId && m.UserId == user.GetUserId(), ct);Covered in: Authentication · OAuth & OIDC · Web security · ASP.NET Core auth.
Milestone 5 — Background notifications
Assigning a task must not wait for the email server:
- Define
IEmailSenderwith a development implementation that logs instead of sending. - When a task's assignee changes, write a
TaskAssignedmessage to a boundedChannel<T>(singleton). - A
BackgroundServicereads the channel and sends the email, with a retry (exponential backoff) and logging. - Stretch: replace the channel with an outbox table written in the same transaction, so no notification is lost on a crash.
Covered in: Thread safety and channels · Queues and background jobs · Resilience · Dependency injection.
Milestone 6 — Tests
- Unit tests for pure logic (validation rules, status transitions).
- Integration tests with
WebApplicationFactory<Program>(addpublic partial class Program;at the end ofProgram.cs) and a real PostgreSQL via Testcontainers:- creating a task returns
201and aLocationheader, - invalid input returns
400, - a non-member gets
404, - assigning a task enqueues exactly one notification (replace
IEmailSenderwith a spy).
- creating a task returns
dotnet add tests/TaskFlow.Api.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add tests/TaskFlow.Api.Tests package Testcontainers.PostgreSql
dotnet testCovered in: Unit testing · Test doubles · TDD and integration testing.
Milestone 7 — Polish
- Structured logging for important events (task created, assigned, deleted).
/healthendpoint with a database check.- OpenAPI document and a
.httpfile with example requests. - Rate limiting on
/auth/login. - Review your queries in the SQL log: no N+1, indexes used.
Stuck?
Go back to the lesson listed under each milestone, or try the idea in this course's playground first. Building it yourself — including the debugging — is where the learning happens.
Checklist
- Solution, projects and PostgreSQL running locally; secrets in user secrets
- Entities, DbContext, indexes and migrations applied
- All endpoints from the design with DTOs, validation and correct status codes
- JWT authentication, membership checks and owner-only operations
- Assignment emails sent from a background service with retries
- Unit and integration tests passing with
dotnet test - Health check, logging, OpenAPI and rate limiting
Mark this lesson complete, then continue to Capstone 3: ship.
Finished reading? Mark the lesson as complete to track your progress.