Skip to content

Commit 0a0369d

Browse files
iammukeshmclaude
andcommitted
Enforce Result pattern in all scaffold and VSA examples
Handlers now return Result<T> instead of raw response types. Endpoints map Result to HTTP: success → TypedResults, failure → ToProblemDetails(). Also fixed: TypedResults over Results, CancellationToken in endpoint lambdas, OpenAPI metadata + ValidationFilter on VSA endpoint examples. Added Result pattern as first item in scaffold checklist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3c25a52 commit 0a0369d

2 files changed

Lines changed: 27 additions & 27 deletions

File tree

skills/scaffolding/SKILL.md

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,24 @@ description: >
2222

2323
Every scaffolded feature MUST include ALL of the following. Do not skip any item:
2424

25+
- [ ] **Result pattern** — Handlers return `Result<T>`, not raw responses. Endpoints map Result to HTTP (success → TypedResults, failure → `ToProblemDetails()`)
2526
- [ ] **CancellationToken** on every async method and passed to every async call
2627
- [ ] **FluentValidation** validator class with meaningful rules (ranges, required fields, max lengths)
2728
- [ ] **ValidationFilter wiring**`.AddEndpointFilter<ValidationFilter<T>>()` on mutating endpoints
2829
- [ ] **OpenAPI metadata**`.WithName()`, `.WithSummary()`, `.Produces<T>()`, `.ProducesValidationProblem()`, `.ProducesProblem(404)`
2930
- [ ] **Pagination** on list endpoints — `page`, `pageSize` with bounded max (e.g., 50)
30-
- [ ] **Global error handler** — Verify `app.UseExceptionHandler()` + `IExceptionHandler` exists in Program.cs; scaffold it if missing
31-
- [ ] **appsettings.json** — Verify connection string exists; scaffold it with placeholder if missing
31+
- [ ] **Global error handler** — Verify `app.UseExceptionHandler()` exists in Program.cs; scaffold if missing
32+
- [ ] **appsettings.json** — Verify connection string exists; scaffold with placeholder if missing
3233
- [ ] **Integration test** with proper DI replacement using `services.RemoveAll<DbContextOptions<T>>()`
3334

3435
## Patterns
3536

3637
### Feature Scaffold — Vertical Slice Architecture (VSA)
3738

38-
Single-file feature with command, handler, validator, and response:
39+
Single-file feature with Result pattern, validation, and response:
3940

4041
```csharp
41-
// Features/Orders/CreateOrder.cs
42+
// Features/Orders/CreateOrder.cs — handler returns Result<T>, not raw response
4243
namespace MyApp.Features.Orders;
4344

4445
public static class CreateOrder
@@ -49,12 +50,12 @@ public static class CreateOrder
4950

5051
internal sealed class Handler(AppDbContext db, TimeProvider clock)
5152
{
52-
public async Task<Response> HandleAsync(Command command, CancellationToken ct)
53+
public async Task<Result<Response>> HandleAsync(Command command, CancellationToken ct)
5354
{
5455
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
5556
db.Orders.Add(order);
5657
await db.SaveChangesAsync(ct);
57-
return new Response(order.Id, order.Total, order.CreatedAt);
58+
return Result.Success(new Response(order.Id, order.Total, order.CreatedAt));
5859
}
5960
}
6061

@@ -74,7 +75,7 @@ public static class CreateOrder
7475
}
7576
```
7677

77-
Endpoint group with full OpenAPI metadata, validation filter, and pagination:
78+
Endpoint group — maps Result to HTTP, full OpenAPI metadata, validation, pagination:
7879

7980
```csharp
8081
// Features/Orders/OrderEndpoints.cs — auto-discovered via IEndpointGroup
@@ -85,28 +86,27 @@ public sealed class OrderEndpoints : IEndpointGroup
8586
var group = app.MapGroup("/api/orders").WithTags("Orders");
8687

8788
group.MapPost("/", CreateOrderHandler)
88-
.WithName("CreateOrder")
89-
.WithSummary("Create a new order")
89+
.WithName("CreateOrder").WithSummary("Create a new order")
9090
.Produces<CreateOrder.Response>(StatusCodes.Status201Created)
9191
.ProducesValidationProblem()
9292
.AddEndpointFilter<ValidationFilter<CreateOrder.Command>>();
9393

9494
group.MapGet("/", ListOrdersHandler)
95-
.WithName("ListOrders")
96-
.WithSummary("List orders with pagination")
95+
.WithName("ListOrders").WithSummary("List orders with pagination")
9796
.Produces<PagedList<OrderSummary>>();
9897

9998
group.MapGet("/{id:guid}", GetOrderHandler)
10099
.WithName("GetOrder")
101-
.Produces<OrderDetail>()
102-
.ProducesProblem(StatusCodes.Status404NotFound);
100+
.Produces<OrderDetail>().ProducesProblem(StatusCodes.Status404NotFound);
103101
}
104102

105-
private static async Task<Created<CreateOrder.Response>> CreateOrderHandler(
103+
private static async Task<IResult> CreateOrderHandler(
106104
CreateOrder.Command cmd, CreateOrder.Handler handler, CancellationToken ct)
107105
{
108-
var response = await handler.HandleAsync(cmd, ct);
109-
return TypedResults.Created($"/api/orders/{response.Id}", response);
106+
var result = await handler.HandleAsync(cmd, ct);
107+
return result.IsSuccess
108+
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
109+
: result.ToProblemDetails();
110110
}
111111

112112
private static async Task<Ok<PagedList<OrderSummary>>> ListOrdersHandler(
@@ -134,10 +134,8 @@ public record PaginationQuery(int Page = 1, int PageSize = 20)
134134
public int Page { get; init; } = Math.Max(1, Page);
135135
public int PageSize { get; init; } = Math.Clamp(PageSize, 1, 50);
136136
}
137-
138137
public record PagedList<T>(List<T> Items, int TotalCount, int Page, int PageSize);
139138
```
140-
```
141139

142140
### Feature Scaffold — Clean Architecture (CA)
143141

skills/vertical-slice/SKILL.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,16 @@ public sealed class OrderEndpoints : IEndpointGroup
9595
{
9696
var group = app.MapGroup("/api/orders").WithTags("Orders");
9797

98-
group.MapPost("/", async (CreateOrder.Command command, ISender sender) =>
98+
group.MapPost("/", async (CreateOrder.Command command, ISender sender, CancellationToken ct) =>
9999
{
100-
var result = await sender.Send(command);
100+
var result = await sender.Send(command, ct);
101101
return result.IsSuccess
102-
? Results.Created($"/api/orders/{result.Value.Id}", result.Value)
102+
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
103103
: result.ToProblemDetails();
104-
});
104+
})
105+
.WithName("CreateOrder").Produces<CreateOrder.OrderResponse>(201)
106+
.ProducesValidationProblem()
107+
.AddEndpointFilter<ValidationFilter<CreateOrder.Command>>();
105108
}
106109
}
107110
```
@@ -122,7 +125,7 @@ public static class CreateOrder
122125
public record OrderResponse(Guid Id, decimal Total, DateTime CreatedAt);
123126

124127
// Wolverine discovers this by convention (static Handle method)
125-
public static async Task<OrderResponse> Handle(
128+
public static async Task<Result<OrderResponse>> Handle(
126129
Command command,
127130
AppDbContext db,
128131
TimeProvider clock,
@@ -131,8 +134,7 @@ public static class CreateOrder
131134
var order = Order.Create(command.CustomerId, command.Items, clock.GetUtcNow());
132135
db.Orders.Add(order);
133136
await db.SaveChangesAsync(ct);
134-
135-
return new OrderResponse(order.Id, order.Total, order.CreatedAt);
137+
return Result.Success(new OrderResponse(order.Id, order.Total, order.CreatedAt));
136138
}
137139
}
138140
```
@@ -165,12 +167,12 @@ public static class CreateOrder
165167
}
166168
}
167169

168-
// Endpoint wiring
170+
// Endpoint wiring — Result maps to HTTP response
169171
group.MapPost("/", async (CreateOrder.Command command, CreateOrder.Handler handler, CancellationToken ct) =>
170172
{
171173
var result = await handler.ExecuteAsync(command, ct);
172174
return result.IsSuccess
173-
? Results.Created($"/orders/{result.Value.Id}", result.Value)
175+
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
174176
: result.ToProblemDetails();
175177
});
176178
```

0 commit comments

Comments
 (0)