Capstone โ Full C# Production App
Bring the tutorial together by building a small Product Catalog API with a domain model, EF Core persistence, REST endpoints, automated tests, Docker packaging, and a CI workflow.
Part 1: Capstone Goal & Solution Structure
This final lesson combines the major ideas from the tutorial into one maintainable application rather than another isolated syntax example.
Solution: create a blank solution named ProductCatalog with these projects:
| Project | Purpose |
|---|---|
ProductCatalog.Domain | Business model and rules |
ProductCatalog.Data | EF Core DbContext and persistence |
ProductCatalog.Api | ASP.NET Core REST API |
ProductCatalog.Tests | Automated tests |
Add project references from Data โ Domain, Api โ Domain + Data, and Tests โ Api. Install Microsoft.EntityFrameworkCore.Sqlite in the Data project and Microsoft.AspNetCore.Mvc.Testing in the test project.
Part 2: Domain Model & EF Core
Start with a domain object that owns at least one business rule.
namespace ProductCatalog.Domain;
public sealed class Product
{
public int Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public void ChangeStock(int quantity)
{
if (quantity < 0)
throw new ArgumentOutOfRangeException(nameof(quantity));
Stock = quantity;
}
}Then add the EF Core context in the Data project.
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Domain;
namespace ProductCatalog.Data;
public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}Part 3: Build the REST API
In ProductCatalog.Api/Program.cs, register EF Core and expose focused endpoints for querying, creating, and updating products.
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data;
using ProductCatalog.Domain;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=products.db"));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.EnsureCreatedAsync();
}
app.MapGet("/api/products", async (AppDbContext db) =>
await db.Products.AsNoTracking().ToListAsync());
app.MapGet("/api/products/{id:int}", async (int id, AppDbContext db) =>
{
Product? product = await db.Products.FindAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
if (string.IsNullOrWhiteSpace(product.Name) || product.Price < 0)
return Results.BadRequest("Name and a non-negative price are required.");
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{product.Id}", product);
});
app.MapPut("/api/products/{id:int}/stock/{quantity:int}",
async (int id, int quantity, AppDbContext db) =>
{
Product? product = await db.Products.FindAsync(id);
if (product is null) return Results.NotFound();
product.ChangeStock(quantity);
await db.SaveChangesAsync();
return Results.NoContent();
});
app.Run();
public partial class Program { }Run the API, then test GET /api/products and POST /api/products with the Visual Studio HTTP file, browser tooling, or another REST client.
Part 4: Add an Automated Integration Test
A production-style application should prove that important endpoints continue to work after code changes.
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;
public sealed class ProductApiTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsSuccess()
{
HttpResponseMessage response =
await _client.GetAsync("/api/products");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}The partial Program declaration in the API makes the top-level application entry point accessible to WebApplicationFactory<Program>.
Part 5: Containerize and Add CI
Place a Dockerfile at the solution root. The multi-stage build compiles with the SDK image and runs with the smaller ASP.NET runtime image.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY . . RUN dotnet publish ProductCatalog.Api/ProductCatalog.Api.csproj -c Release -o /app/publish FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app COPY --from=build /app/publish . EXPOSE 8080 ENTRYPOINT ["dotnet", "ProductCatalog.Api.dll"]
Then create .github/workflows/build.yml so every push and pull request restores, builds, and tests the solution.
name: build-and-test
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c ReleasePart 6: Production Improvements & Final Challenge
The capstone is intentionally small enough to understand. Before calling a real service production-ready, continue improving it:
- Replace
EnsureCreatedAsync()with EF Core migrations. - Add DTOs and validation instead of accepting persistence entities directly from clients.
- Add structured logging, exception handling, OpenAPI documentation, health checks, and configuration.
- Add authentication and authorization where required.
- Use a production database and protect secrets with environment-specific configuration.
- Add more unit and integration tests, including failure cases.
- Build and scan the Docker image in CI before deployment.
Final challenge: add categories, product search, low-stock reporting, pagination, and a second integration test. Commit the completed solution to GitHub and treat it as the portfolio project for this 40-lesson C# tutorial.
Congratulations: you have reached Lesson 40. Use the tutorial index to revisit any topic and keep extending this capstone as your C# skills grow.
C# in Visual Studio 2026
๐ This lesson is part of the book C# in Visual Studio 2026 by Dr. Liew Voon Kiong.
View on Amazon Kindle Edition