Architecture
NetForge is held together by five ideas. Internalize these and the whole codebase becomes predictable — which is exactly what makes it easy for both humans and AI assistants to extend.
The five ideas
- Backend = vertical slices. Every feature lives under
Features/{Domain}/and owns itsEndpoints.cs,Models.cs,Validators.cs,EfConfig.cs,Mappings.cs, andPermissions.cs. Slices register themselves by reflection at startup — you never editProgram.csto add a feature. - Frontend = file-system routes. Screens live under
src/pages/, and the folder tree is the URL tree. A_-prefixed file or directory is ignored by the router. - Errors = RFC 7807 ProblemDetails, always. Never
throw new Exception(...)— throw aDomainExceptionsubclass and the global handler maps it to a clean JSON problem with atraceId. - Lists =
PagedRequest/PagedResult<T>with an operator-suffix query syntax (?price=gte:10&status=in:a,b&sort=name:asc). On the frontend,useDataGrid()+<DataGrid>consume it. See Data tables. - Cross-cutting concerns = an
IEndpointFilterpipeline — validation, audit, performance, and transactions are applied per slice, not re-implemented per handler.
Anatomy of a slice
A minimal-API handler is the handler — there's no MediatR indirection:
group.MapPost("/", async (CreateProductRequest req, AppDbContext db, CancellationToken ct) =>
{
var product = Product.Create(req); // plain mapping, no AutoMapper
db.Products.Add(product);
await db.SaveChangesAsync(ct);
return Results.Created($"/api/products/{product.Id}", product.ToDto());
})
.RequirePermission(ProductPermissions.Create);The slice exposes its routes through a marker interface that the startup discovers by reflection, so adding Features/Projects/ is enough — no registration step.
The filter pipeline
Each slice's route group opts into the shared filters:
- Validation — finds the request's FluentValidation validator, runs it, and returns a 400 ProblemDetails on failure (field errors surface inline on forms).
- Audit — records a trail entry for successful writes (see Audit).
- Performance — warns on slow handlers and adds an
X-Response-Timeheader. - Transaction — wraps write handlers in a database transaction (applied per-handler, so reads don't pay the cost).
What NetForge deliberately avoids
No MediatR/CQRS ceremony, no AutoMapper, no repository layer over EF, no Server Components/SSR, no DDD aggregates. Minimal-API handlers are the handlers, mapping is static methods, and you use DbContext directly. Fewer abstractions, less to learn, faster to extend.
Build your first slice
The Add a feature recipe walks the whole loop — copy _Template, rename, add a migration, and the slice auto-registers.