Skip to content

Add a feature

A feature is a backend vertical slice plus a frontend screen. Both start by copying a _Template, so there's no blank page — you rename and fill in.

1. The backend slice

A slice is six files under Features/{Domain}/. Copy the template:

bash
cp -r NetForge.Server/Features/_Template NetForge.Server/Features/Projects

Rename TemplateProject (and templateproject) throughout. Here's what each file becomes.

Models.cs — entity + DTO + request records:

csharp
public class Project
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public string? Description { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
}

public record ProjectDto(int Id, string Name, string? Description, DateTimeOffset CreatedAt);
public record CreateProjectRequest(string Name, string? Description);
public record UpdateProjectRequest(string Name, string? Description);

Permissions.cs — one constant per action ([Description] feeds the admin catalog):

csharp
public static class ProjectPermissions
{
    [Description("View projects")]   public const string Read   = "projects.read";
    [Description("Create projects")] public const string Create = "projects.create";
    [Description("Edit projects")]   public const string Update = "projects.update";
    [Description("Delete projects")] public const string Delete = "projects.delete";
}

Validators.cs — an internal sealed validator per request (the ValidationFilter runs it automatically):

csharp
internal sealed class CreateProjectValidator : AbstractValidator<CreateProjectRequest>
{
    public CreateProjectValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Description).MaximumLength(1000);
    }
}

Mappings.cs — static extension, no AutoMapper:

csharp
internal static class ProjectMappings
{
    public static ProjectDto ToDto(this Project e) => new(e.Id, e.Name, e.Description, e.CreatedAt);
}

EfConfig.csIEntityTypeConfiguration<T>:

csharp
internal sealed class ProjectConfig : IEntityTypeConfiguration<Project>
{
    public void Configure(EntityTypeBuilder<Project> b)
    {
        b.ToTable("Projects");
        b.HasKey(x => x.Id);
        b.Property(x => x.Name).HasMaxLength(200).IsRequired();
    }
}

Endpoints.cs — the IFeatureEndpoints implementation. Note the filter placement: validation/performance per group, transaction per write handler:

csharp
public sealed class ProjectEndpoints : IFeatureEndpoints
{
    public void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/projects")
            .WithTags("Projects")
            .AddEndpointFilter<ValidationFilter>()
            .AddEndpointFilter<PerformanceFilter>();

        group.MapGet("/", List).RequirePermission(ProjectPermissions.Read);
        group.MapGet("/{id:int}", Get).RequirePermission(ProjectPermissions.Read);
        group.MapPost("/", Create).RequirePermission(ProjectPermissions.Create).AddEndpointFilter<TransactionFilter>();
        group.MapPut("/{id:int}", Update).RequirePermission(ProjectPermissions.Update).AddEndpointFilter<TransactionFilter>();
        group.MapDelete("/{id:int}", Delete).RequirePermission(ProjectPermissions.Delete).AddEndpointFilter<TransactionFilter>();
    }

    private static async Task<IResult> Get(int id, AppDbContext db, CancellationToken ct) =>
        await db.Set<Project>().AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct) is { } p
            ? Results.Ok(p.ToDto())
            : throw new NotFoundException(nameof(Project), id);   // → 404 ProblemDetails
    // … List / Create / Update / Delete as in _Template
}

Add the migration:

bash
dotnet ef migrations add AddProjects --project NetForge.Server

The slice is now live — you never touched Program.cs. Feature discovery found it by reflection. Assign the new permissions to a role at /admin/roles.

Ordering

If a slice must register before/after others, add [FeatureOrder(n)] to the endpoints class (default 100).

2. The frontend screen

Routes are files under src/pages/. Copy the template:

bash
cp -r netforge.client/src/pages/_template "netforge.client/src/pages/(app)/projects"

Rename, edit meta.ts ({ title, permissions }), and hand-author a typed API module at src/lib/api/projects.ts mirroring the existing ones (interfaces + a projectsApi object over the shared api client). Routes regenerate automatically — there's no route config to touch.

Next

NetForge Community is MIT-licensed. Pro is a commercial edition.