scaffolding
Skill229 starsChanged 3 months agoApache-2.0View on GitHub
Code scaffolding patterns for .NET 10 features, entities, and tests. Generates complete feature slices, entities with EF Core configuration, and integration tests following the project's chosen architecture. Load when: "scaffold", "create feature", "add feature", "new endpoint", "generate", "add entity", "new entity", "scaffold test", "add module".
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 23
Every scaffolded feature MUST include ALL of the following.
- Very specific: line 234
Always pair entity + IEntityTypeConfiguration<T>.
- Very specific: line 263
After creating entity + config: dotnet ef migrations add AddProduct
- Very specific: line 365
Always scaffold feature + test as a single unit.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Scaffolding
Core Principles
- Architecture-aware generation — Never scaffold without knowing the project's architecture (VSA, CA, DDD, Modular Monolith). If unknown, ask first or run the architecture-advisor questionnaire.
- Complete vertical slices — Never generate half a feature. A scaffold includes endpoint, handler, validation, DTOs, EF configuration, and tests as a single unit.
- Tests included by default — Every scaffolded feature includes at least one integration test using
WebApplicationFactory+Testcontainers. Skip only if explicitly told to. - Modern C# 14 patterns — Primary constructors, collection expressions,
file-scoped types, records for DTOs,sealedon all handler classes. - Convention-matching — Before generating, check existing code for naming patterns (
*Handler,*Service,*Endpoint), folder structure, and access modifiers. Match what exists.
Scaffold Checklist (MANDATORY)
Every scaffolded feature MUST include ALL of the following. Do not skip any item:
- Result pattern — Handlers return
Result<T>, not raw responses. Endpoints map Result to HTTP (success → TypedResults, failure →ToProblemDetails()) - CancellationToken on every async method and passed to every async call
- FluentValidation validator class with meaningful rules (ranges, required fields, max lengths)
- ValidationFilter wiring —
.AddEndpointFilter<ValidationFilter<T>>()on mutating endpoints - OpenAPI metadata —
.WithName(),.WithSummary(),.Produces<T>(),.ProducesValidationProblem(),.ProducesProblem(404) - Pagination on list endpoints —
page,pageSizewith bounded max (e.g., 50) - Global error handler — Verify
app.UseExceptionHandler()exists in Program.cs; scaffold if missing - appsettings.json — Verify connection string exists; scaffold with placeholder if missing
- Integration test with proper DI replacement using
services.RemoveAll<DbContextOptions<T>>()
Patterns
Feature Scaffold — Vertical Slice Architecture (VSA)
Single-file feature with Result pattern, validation, and response:
// Features/Orders/CreateOrder.cs — handler returns Result<T>, not raw response
namespace MyApp.Features.Orders;
public static class CreateOrder
{
public record Command(string CustomerId, List<ItemDto> Items);
public record ItemDto(Guid ProductId, int Quantity, decimal UnitPrice);
public record Response(Guid Id, decimal Total, DateTimeOffset CreatedAt);
internal sealed class Handler(AppDbContext db, TimeProvider clock)
{
public async Task<Result<Response>> HandleAsync(Command command, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(new Response(order.Id, order.Total, order.CreatedAt));
}
}
internal sealed class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.Quantity).InclusiveBetween(1, 1000);
item.RuleFor(x => x.UnitPrice).GreaterThan(0);
});
}
}
}Endpoint group — maps Result to HTTP, full OpenAPI metadata, validation, pagination:
// Features/Orders/OrderEndpoints.cs — auto-discovered via IEndpointGroup
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrderHandler)
.WithName("CreateOrder").WithSummary("Create a new order")
.Produces<CreateOrder.Response>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.AddEndpointFilter<ValidationFilter<CreateOrder.Command>>();
group.MapGet("/", ListOrdersHandler)
.WithName("ListOrders").WithSummary("List orders with pagination")
.Produces<PagedList<OrderSummary>>();
group.MapGet("/{id:guid}", GetOrderHandler)
.WithName("GetOrder")
.Produces<OrderDetail>().ProducesProblem(StatusCodes.Status404NotFound);
}
private static async Task<IResult> CreateOrderHandler(
CreateOrder.Command cmd, CreateOrder.Handler handler, CancellationToken ct)
{
var result = await handler.HandleAsync(cmd, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: result.ToProblemDetails();
}
private static async Task<Ok<PagedList<OrderSummary>>> ListOrdersHandler(
[AsParameters] PaginationQuery paging, AppDbContext db, CancellationToken ct)
{
var query = db.Orders.OrderByDescending(o => o.CreatedAt);
var total = await query.CountAsync(ct);
var items = await query.Skip((paging.Page - 1) * paging.PageSize).Take(paging.PageSize)
.Select(o => new OrderSummary(o.Id, o.Total, o.CreatedAt)).ToListAsync(ct);
return TypedResults.Ok(new PagedList<OrderSummary>(items, total, paging.Page, paging.PageSize));
}
private static async Task<Results<Ok<OrderDetail>, NotFound>> GetOrderHandler(
Guid id, AppDbContext db, CancellationToken ct)
{
var order = await db.Orders.Where(o => o.Id == id)
.Select(o => new OrderDetail(o.Id, o.CustomerId, o.Total, o.CreatedAt)).FirstOrDefaultAsync(ct);
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}
}
// Common/PaginationQuery.cs
public record PaginationQuery(int Page = 1, int PageSize = 20)
{
public int Page { get; init; } = Math.Max(1, Page);
public int PageSize { get; init; } = Math.Clamp(PageSize, 1, 50);
}
public record PagedList<T>(List<T> Items, int TotalCount, int Page, int PageSize);Feature Scaffold — Clean Architecture (CA)
Separate files across layers. Domain → Application (Command + Handler + Validator) → Api (Endpoint):
// Application/Orders/CreateOrder/CreateOrderCommand.cs — uses Mediator (source-generated, MIT)
public record CreateOrderCommand(string CustomerId, List<OrderItemDto> Items) : IRequest<Result<CreateOrderResponse>>;
public record CreateOrderResponse(Guid Id, decimal Total, DateTimeOffset CreatedAt);
// Application/Orders/CreateOrder/CreateOrderHandler.cs
internal sealed class CreateOrderHandler(IAppDbContext db, TimeProvider clock)
: IRequestHandler<CreateOrderCommand, Result<CreateOrderResponse>>
{
public async ValueTask<Result<CreateOrderResponse>> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(request.CustomerId, request.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return new CreateOrderResponse(order.Id, order.Total, order.CreatedAt);
}
}
// Api/Endpoints/OrderEndpoints.cs — auto-discovered via IEndpointGroup
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", async (CreateOrderCommand cmd, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(cmd, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: result.ToProblemDetails();
})
.WithName("CreateOrder").Produces<CreateOrderResponse>(201)
.ProducesValidationProblem()
.AddEndpointFilter<ValidationFilter<CreateOrderCommand>>();
}
}Feature Scaffold — DDD
Domain logic lives in the aggregate; handler orchestrates persistence:
// Domain/Orders/Order.cs — Aggregate root with invariant enforcement
public sealed class Order : AggregateRoot
{
private readonly List<OrderItem> _items = [];
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public decimal Total { get; private set; }
public OrderStatus Status { get; private set; }
public static Order Place(string customerId, List<(Guid ProductId, int Qty, decimal Price)> items, DateTimeOffset now)
{
if (items.Count == 0) throw new DomainException("Order must have at least one item.");
var order = new Order { Id = Guid.NewGuid(), Status = OrderStatus.Placed };
foreach (var (productId, qty, price) in items)
order._items.Add(OrderItem.Create(productId, qty, price));
order.Total = order._items.Sum(i => i.LineTotal);
order.AddDomainEvent(new OrderPlacedEvent(order.Id, customerId, order.Total, now));
return order;
}
}Feature Scaffold — Modular Monolith
Feature within a module boundary with its own DbContext. Handler passes CancellationToken, publishes integration events:
// Modules/Orders/Features/PlaceOrder.cs
public static class PlaceOrder
{
public record Command(string CustomerId, List<ItemDto> Items);
public record Response(Guid OrderId, decimal Total);
internal sealed class Handler(OrdersDbContext db, TimeProvider clock, IEventBus bus)
{
public async Task<Response> HandleAsync(Command command, CancellationToken ct)
{
var order = Order.Place(command.CustomerId, command.Items, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderPlacedIntegrationEvent(order.Id, order.Total), ct);
return new Response(order.Id, order.Total);
}
}
}Entity Scaffold
Always pair entity + IEntityTypeConfiguration<T>. No data annotations on entities.
// Domain/Entities/Product.cs — clean, no attributes
public sealed class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; } = string.Empty;
public string Sku { get; private set; } = string.Empty;
public decimal Price { get; private set; }
public static Product Create(string name, string sku, decimal price) =>
new() { Id = Guid.NewGuid(), Name = name, Sku = sku, Price = price };
}
// Persistence/Configurations/ProductConfiguration.cs — all EF config here
internal sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(200).IsRequired();
builder.Property(x => x.Sku).HasMaxLength(50).IsRequired();
builder.HasIndex(x => x.Sku).IsUnique();
builder.Property(x => x.Price).HasPrecision(18, 2);
}
}After creating entity + config: dotnet ef migrations add AddProduct
Test Scaffold
Integration test with proper DI replacement (RemoveAll, not fragile name matching):
// Tests/Fixtures/ApiFixture.cs
public sealed class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().WithImage("postgres:17").Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(o => o.UseNpgsql(_postgres.GetConnectionString()));
});
}
public async Task InitializeAsync() { await _postgres.StartAsync(); /* apply migrations */ }
public new async Task DisposeAsync() { await _postgres.DisposeAsync(); await base.DisposeAsync(); }
}// Tests/Features/Orders/CreateOrderTests.cs
public sealed class CreateOrderTests(ApiFixture fixture) : IClassFixture<ApiFixture>
{
private readonly HttpClient _client = fixture.CreateClient();
[Fact]
public async Task CreateOrder_ValidRequest_Returns201()
{
// Arrange
var command = new { CustomerId = "CUST-001", Items = new[] { new { ProductId = Guid.NewGuid(), Quantity = 2, UnitPrice = 29.99m } } };
// Act
var response = await _client.PostAsJsonAsync("/api/orders", command);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.NotEqual(Guid.Empty, result.GetProperty("id").GetGuid());
}
[Fact]
public async Task CreateOrder_EmptyItems_ReturnsValidationProblem()
{
var response = await _client.PostAsJsonAsync("/api/orders", new { CustomerId = "CUST-001", Items = Array.Empty<object>() });
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}Module Scaffold (Modular Monolith)
Module = DI registration class + IEndpointGroup endpoints + own DbContext with isolated schema:
// Modules/Inventory/InventoryModule.cs — DI only, no endpoint wiring
public static class InventoryModule
{
public static IServiceCollection AddInventoryModule(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<InventoryDbContext>(o => o.UseNpgsql(config.GetConnectionString("Inventory")));
return services;
}
}
// Modules/Inventory/Endpoints/InventoryEndpoints.cs — auto-discovered via IEndpointGroup
public sealed class InventoryEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/inventory").WithTags("Inventory");
// endpoint definitions with full OpenAPI metadata + ValidationFilter
}
}
// Modules/Inventory/Persistence/InventoryDbContext.cs — isolated schema
internal sealed class InventoryDbContext(DbContextOptions<InventoryDbContext> options) : DbContext(options)
{
public DbSet<StockItem> StockItems => Set<StockItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("inventory");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(InventoryDbContext).Assembly);
}
}Anti-patterns
Scaffolding Without Architecture
// BAD — Generating code without knowing if project uses VSA, CA, or DDD
public class CreateOrderHandler { /* random structure */ }
// GOOD — Ask first: "I see feature folders, so I'll scaffold using VSA patterns."
public static class CreateOrder { /* VSA single-file feature */ }Feature Without Tests
Always scaffold feature + test as a single unit. CreateOrder.cs + CreateOrderTests.cs are never generated separately.
Entity Without EF Configuration
// BAD — data annotations scattered in entity
public class Product { [Key] public Guid Id { get; set; } [MaxLength(200)] public string Name { get; set; } = ""; }
// GOOD — clean entity + separate IEntityTypeConfiguration<T>
public sealed class Product { /* No attributes */ }
internal sealed class ProductConfiguration : IEntityTypeConfiguration<Product> { /* All EF config */ }Anemic DTOs That Mirror Entities 1:1
// BAD — DTO mirrors entity with no purpose
public record ProductDto(Guid Id, string Name, string Sku, decimal Price, bool IsActive, DateTime CreatedAt, DateTime? UpdatedAt);
// GOOD — response shaped for the consumer
public record ProductSummary(Guid Id, string Name, decimal Price);Decision Guide
| Scenario | Architecture | Scaffold Pattern |
| New CRUD endpoint | VSA | Single-file feature (Command + Handler + Validator + Response) |
| New business operation | CA | Command in Application/, Handler in Application/, Endpoint in Api/ |
| Complex domain logic | DDD | Aggregate method + Application handler + Domain event |
| Feature in a module | Modular Monolith | Feature file in Modules/{Name}/Features/ with module DbContext |
| New entity | Any | Entity class + IEntityTypeConfiguration<T> + migration |
| New module | Modular Monolith | Module folder + DbContext + DI registration + integration events |
| Architecture unknown | Any | Ask first — run architecture-advisor questionnaire |
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- dotnet/efcore★ 15ksame name
- korallis/skills-collection★ 14same name
- renathoaz/arsyn★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository96 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- 80-20-reviewSkill
- api-versioningSkill
- architecture-advisorSkill
- aspireSkill
- authenticationSkill
- autonomous-loopsSkill
- cachingSkill
- ci-cdSkill
- clean-architectureSkill
- code-review-workflowSkill
- configurationSkill
- container-publishSkill
- context-disciplineSkill
- convention-learnerSkill
- dddSkill
- dependency-injectionSkill
- de-sloppifySkill
- dockerSkill
- ef-coreSkill
- error-handlingSkill
- health-checkSkill
- httpclient-factorySkill
- instinct-systemSkill
- learning-logSkill
- loggingSkill
- messagingSkill
- migration-workflowSkill
- minimal-apiSkill
- model-selectionSkill
- modern-csharpSkill
- openapiSkill
- opentelemetrySkill
- project-setupSkill
- project-structureSkill
- resilienceSkill
- scalarSkill
- security-scanSkill
- self-correction-loopSkill
- serilogSkill
- session-managementSkill
- split-memorySkill
- testingSkill
- verification-loopSkill
- vertical-sliceSkill
- workflow-masterySkill
- wrap-up-ritualSkill
- 80-20-reviewSkill
- api-versioningSkill
- architecture-advisorSkill
- aspireSkill
- authenticationSkill
- autonomous-loopsSkill
- cachingSkill
- ci-cdSkill
- clean-architectureSkill
- code-review-workflowSkill
- configurationSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

