Complete CRUD Project
Put the whole track together: a small but production-shaped Products API using only ADO.NET.
On this page · 5 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
The final project assembles every part you learned into one working machine: a Products API that creates, reads, updates and deletes data safely.
- CRUD
- Create, Read, Update, Delete.
- Status codes
- 201 created, 204 no content, 404, 409.
- Layers
- Endpoint → repository → SQL Server.
Learning objectives#
- Structure a project with a repository and DI
- Expose CRUD endpoints with correct status codes
- Validate input and handle SQL errors
- Test it end to end
Explanation#
Project layout: Program.cs (endpoints), Data/SqlConnectionFactory.cs, Data/ProductRepository.cs, Models/Product.cs and database/schema.sql.
Endpoints: GET /api/products?page=1&size=20, GET /api/products/{id}, POST, PUT /api/products/{id} and DELETE /api/products/{id}.
Validation happens before SQL; duplicate names (unique index) become 409 Conflict; missing rows become 404.
Extensions: an order endpoint using the dbo.PlaceOrder procedure inside a transaction, and a CSV import with SqlBulkCopy.
Code example: Program.cs — the endpoints#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDbConnectionFactory, SqlConnectionFactory>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
var app = builder.Build();
var products = app.MapGroup("/api/products");
products.MapGet("/", (IProductRepository repo, CancellationToken ct) => repo.GetAllAsync(ct));
products.MapGet("/{id:int}", async (int id, IProductRepository repo, CancellationToken ct) =>
await repo.GetByIdAsync(id, ct) is { } p ? Results.Ok(p) : Results.NotFound());
products.MapPost("/", async (Product p, IProductRepository repo, CancellationToken ct) =>
{
if (string.IsNullOrWhiteSpace(p.Name) || p.Price <= 0 || p.Stock < 0)
return Results.ValidationProblem(new Dictionary<string, string[]> { ["product"] = ["Name, a positive price and non-negative stock are required."] });
try
{
int id = await repo.CreateAsync(p, ct);
return Results.Created(quot;/api/products/{id}", p with { Id = id });
}
catch (SqlException ex) when (ex.Number is 2627 or 2601)
{
return Results.Conflict(new { message = quot;A product named '{p.Name}' already exists." });
}
});
products.MapPut("/{id:int}", async (int id, Product p, IProductRepository repo, CancellationToken ct) =>
await repo.UpdateAsync(p with { Id = id }, ct) ? Results.NoContent() : Results.NotFound());
products.MapDelete("/{id:int}", async (int id, IProductRepository repo, CancellationToken ct) =>
await repo.DeleteAsync(id, ct) ? Results.NoContent() : Results.NotFound());
app.Run();
POST /api/products {"name":"Webcam","price":3200,"stock":15} → 201 Created, Location: /api/products/4
POST /api/products {"name":"Webcam","price":3200,"stock":15} → 409 Conflict
GET /api/products/999 → 404 Not Found
DELETE /api/products/4 → 204 No ContentThis example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- DI wires the factory and repository from the previous lessons.
- GET by id returns 404 when the repository returns null.
- POST validates first, then maps a unique-index violation to 409.
- PUT and DELETE return 204 or 404 based on rows affected.
Real-world use case#
This project is a strong portfolio piece and the backend for the React and Angular integration lessons on this site.
Common mistakes#
Not found, conflicts and validation errors have their own status codes.
Run the repository against a real SQL Server (for example in Docker) to catch SQL mistakes.
Best practices#
- Unique index on Products(Name)
- Repository behind an interface
- Correct HTTP status codes
- Integration tests with a disposable database
Remember this#
Factory + repository + endpoints = clean ADO.NET API.
404 not found, 409 conflict, 201 created, 204 no content.
Validate before SQL, map SqlException after.
Practice exercise#
Your task: Add a GET /api/products/search?q= endpoint using the SearchAsync method from the repository lesson.
Show solution
products.MapGet("/search", (string q, IProductRepository repo, CancellationToken ct) =>
repo.SearchAsync(q, ct));
Mini quiz#
Which status code should PUT return when the product does not exist?
Want more? Practise all ADO.NET MCQs · ADO.NET interview questions
My notes#
Saved in this browser.