Skip to content

Head.Net

Convention-first EF Core and Minimal API SDK for .NET

Head.Net reduces CRUD boilerplate in internal APIs and admin backends. Declare an entity, describe the surface, attach lifecycle hooks and custom actions — Head.Net provides the HTTP endpoints and EF Core wiring automatically.

The Problem

You're building an internal API. There's an Invoice entity with business rules. Before you write domain logic, you'll spend hours on:

  • Repository interfaces and implementations
  • Service layers
  • Controller routing
  • OpenAPI annotations
  • Response type declarations

All of that is infrastructure you've written a dozen times.

The Solution

Program.cs
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connString));
builder.Services.AddHeadEntityStore<AppDbContext, Invoice>();

var app = builder.Build();

app.MapEntity<Invoice>()
    .WithCrud()
    .Setup<InvoiceSetup>()
    .Build();

app.Run();
InvoiceSetup.cs
public sealed class InvoiceSetup : IHeadEntitySetup<Invoice>
{
    private readonly IEmailService _email;

    public InvoiceSetup(IEmailService email) => _email = email;

    public void Configure(HeadEntityEndpointBuilder<Invoice> builder)
    {
        builder
            .WithPaging(defaultPageSize: 50)
            .BeforeCreate((invoice, _) =>
            {
                invoice.CreatedAt = DateTimeOffset.UtcNow;
                invoice.Status = "draft";
                return ValueTask.CompletedTask;
            })
            .AfterCreate(async (invoice, ct) =>
                await _email.SendConfirmationAsync(invoice.CustomerEmail, ct))
            .RequireOwnership(inv => inv.UserId)
            .CustomAction("pay", async (invoice, ct) =>
            {
                await _billing.ChargeAsync(invoice.Id, invoice.Total, ct);
                invoice.Status = "paid";
                invoice.PaidAt = DateTimeOffset.UtcNow;
            });
    }
}

That's everything. You get these endpoints, fully wired to EF Core and OpenAPI:

Method Route What it does
GET /invoices?skip=0&take=50 Paginated list, scoped to the current user
GET /invoices/{id} Single invoice — 403 if not owner
POST /invoices Create — sets CreatedAt, Status, sends email
PUT /invoices/{id} Update — 403 if not owner
DELETE /invoices/{id} Delete — 403 if not owner
POST /invoices/{id}/pay Domain action — charges and updates status

No repository. No service layer. No controller. No manual route naming.

Three Core Ideas

1. CRUD is Infrastructure

The List/Get/Create/Update/Delete cycle is the same across every entity. Head.Net treats it as generated, not authored.

2. Hooks are Where the Work Lives

The list endpoint for invoices is never the hard part. The hard part is stamping timestamps, sending notifications, enforcing ownership, blocking dangerous deletes. These encode your domain knowledge.

3. Domain Actions Need a Home

Invoices get paid. Documents get archived. These operations don't map naturally to PUT. Head.Net gives them first-class routes.

Where It Fits

Raw Minimal APIs MVC Controllers Head.Net
Boilerplate per entity High Very high Near zero
Lifecycle hooks Manual Manual Built-in
Domain actions Manual Separate controller First-class
OpenAPI Manual Attribute-based Automatic
EF Core wiring Manual Manual Conventions-first
Best suited for Custom APIs Complex routing Internal APIs, admin backends, MVPs

Requirements

  • .NET 8 or .NET 9
  • EF Core 9
  • ASP.NET Core with Minimal APIs
  • Entities implement IHeadEntity<int>

Next Steps