๐Ÿ  VisualStudioTutor.com  ยท  C# Tutorial Home  ยท  C# Lesson 40 of 40
Lesson 40 of 40 Capstone Expert โฑ 60โ€“90 min

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:

ProjectPurpose
ProductCatalog.DomainBusiness model and rules
ProductCatalog.DataEF Core DbContext and persistence
ProductCatalog.ApiASP.NET Core REST API
ProductCatalog.TestsAutomated 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 Release

Part 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