Roadmap › .NET Full Stack Developer Interview — End to End › Module: Building a Feature Full Stack
Lessons in this course (10)

6. Worked example — "Products" CRUD with search and paging

Intermediate★ Must knowLesson 6 of 10

The classic live-coding / take-home task. Build it layer by layer.

1. Database (SQL Server)

sql
CREATE TABLE dbo.Products (
    Id         INT IDENTITY PRIMARY KEY,
    Name       NVARCHAR(200) NOT NULL,
    Category   NVARCHAR(100) NOT NULL,
    Price      DECIMAL(18,2) NOT NULL CHECK (Price >= 0),
    IsActive   BIT NOT NULL DEFAULT 1,
    RowVersion ROWVERSION
);
CREATE INDEX IX_Products_Category_Name ON dbo.Products(Category, Name) INCLUDE (Price) WHERE IsActive = 1;

2. Entity, DTOs and validation

csharp
public class Product { public int Id { get; set; } public string Name { get; set; } = ""; public string Category { get; set; } = "";
    public decimal Price { get; set; } public bool IsActive { get; set; } = true; public byte[] RowVersion { get; set; } = []; }

public record ProductDto(int Id, string Name, string Category, decimal Price);
public record SaveProductRequest(
    [property: Required, MaxLength(200)] string Name,
    [property: Required, MaxLength(100)] string Category,
    [property: Range(0, 1_000_000)] decimal Price);

3. Endpoints (minimal API style)

csharp
var products = app.MapGroup("/api/products").RequireAuthorization();

products.MapGet("/", async (AppDb db, string? q, int page = 1, int size = 20) =>
{
    var query = db.Products.AsNoTracking().Where(p => p.IsActive);
    if (!string.IsNullOrWhiteSpace(q)) query = query.Where(p => p.Name.Contains(q));
    var total = await query.CountAsync();
    var items = await query.OrderBy(p => p.Name).Skip((page - 1) * size).Take(size)
        .Select(p => new ProductDto(p.Id, p.Name, p.Category, p.Price)).ToListAsync();
    return Results.Ok(new { items, total });
});

products.MapPost("/", async (AppDb db, SaveProductRequest r) =>
{
    var p = new Product { Name = r.Name, Category = r.Category, Price = r.Price };
    db.Products.Add(p);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{p.Id}", new ProductDto(p.Id, p.Name, p.Category, p.Price));
});

products.MapDelete("/{id:int}", async (AppDb db, int id) =>
    await db.Products.Where(p => p.Id == id).ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false)) == 1
        ? Results.NoContent() : Results.NotFound());   // soft delete

4. UI (React + TypeScript)

tsx
function Products() {
  const [q, setQ] = useState("");
  const [page, setPage] = useState(1);
  const [data, setData] = useState<{ items: ProductDto[]; total: number }>({ items: [], total: 0 });

  useEffect(() => {
    const ctrl = new AbortController();
    const t = setTimeout(() =>
      fetch(`/api/products?q=${encodeURIComponent(q)}&page=${page}`, { signal: ctrl.signal })
        .then(r => r.json()).then(setData).catch(() => {}), 300);   // debounce typing
    return () => { clearTimeout(t); ctrl.abort(); };
  }, [q, page]);

  return (<>
    <input value={q} onChange={e => { setQ(e.target.value); setPage(1); }} placeholder="Search" />
    <table><tbody>{data.items.map(p => <tr key={p.id}><td>{p.name}</td><td>{p.category}</td><td>{p.price}</td></tr>)}</tbody></table>
    <button disabled={page === 1} onClick={() => setPage(page - 1)}>Prev</button>
    <button disabled={page * 20 >= data.total} onClick={() => setPage(page + 1)}>Next</button>
  </>);
}

Points interviewers look for: DTOs instead of entities over the wire, server-side paging with a max page size, 201 Created with a location, soft delete, debounced search with request cancellation, a concurrency token (ROWVERSION) for edits, and an index that matches the query.