diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 7c7559e1c..d7886d01f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -88,7 +88,10 @@ const config: UserConfig = { {text: 'Dealing with Concurrency', link:'/tutorials/concurrency'}, {text: 'Dead Letter Queues', link: '/tutorials/dead-letter-queues'}, {text: 'Idempotency in Messaging', link: '/tutorials/idempotency'}, - {text: 'Multi-Tenancy', link: '/tutorials/multi-tenancy'} + {text: 'Multi-Tenancy', link: '/tutorials/multi-tenancy'}, + {text: 'Migrating from Minimal APIs', link: '/tutorials/from-minimal-api'}, + {text: 'Migrating from MVC Controllers', link: '/tutorials/from-mvc'}, + {text: 'Migrating from MVC/Minimal API Filters', link: '/tutorials/middleware-migration'} ] }, { diff --git a/docs/tutorials/from-minimal-api.md b/docs/tutorials/from-minimal-api.md new file mode 100644 index 000000000..0ebb45b1c --- /dev/null +++ b/docs/tutorials/from-minimal-api.md @@ -0,0 +1,444 @@ +# Migrating from Minimal APIs to Wolverine.HTTP + +This tutorial provides side-by-side conversions between ASP.NET Core Minimal API endpoints and +their Wolverine.HTTP equivalents. If you're already comfortable with Minimal APIs, this will +get you productive with Wolverine.HTTP quickly. + +::: tip +For filter and middleware migration specifically, see +[Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration). +::: + +## Basic GET Endpoint + +### Minimal API + +```csharp +app.MapGet("/api/orders/{id}", async (int id, IOrderRepository repo) => +{ + var order = await repo.GetByIdAsync(id); + return order is not null ? Results.Ok(order) : Results.NotFound(); +}); +``` + +### Wolverine + +```csharp +public static class GetOrderEndpoint +{ + [WolverineGet("/api/orders/{id}")] + public static async Task Get(int id, IOrderRepository repo) + { + var order = await repo.GetByIdAsync(id); + return order is not null ? Results.Ok(order) : Results.NotFound(); + } +} +``` + +Or more idiomatically with Wolverine's `[Entity]` attribute, which loads the entity and returns 404 automatically: + +```csharp +public static class GetOrderEndpoint +{ + [WolverineGet("/api/orders/{id}")] + public static Order Get([Entity] Order order) => order; +} +``` + +**Key differences:** +- Wolverine endpoints are methods on public classes, decorated with `[WolverineGet]` etc. +- Route parameters, query strings, and services are bound the same way — by parameter name and type +- `IResult` return type works identically +- Wolverine adds `[Entity]` for automatic persistence loading with 404 handling + +## Basic POST Endpoint + +### Minimal API + +```csharp +app.MapPost("/api/orders", async (CreateOrder command, IOrderRepository repo) => +{ + var order = new Order { Id = Guid.NewGuid(), ProductName = command.ProductName }; + await repo.SaveAsync(order); + return Results.Created($"/api/orders/{order.Id}", order); +}); +``` + +### Wolverine + +```csharp +public static class CreateOrderEndpoint +{ + [WolverinePost("/api/orders")] + public static async Task Post( + CreateOrder command, IOrderRepository repo) + { + var order = new Order { Id = Guid.NewGuid(), ProductName = command.ProductName }; + await repo.SaveAsync(order); + + // CreationResponse sets 201 status and Location header automatically + return new CreationResponse($"/api/orders/{order.Id}"); + } +} +``` + +**Key differences:** +- The first complex type parameter is automatically deserialized from the JSON body (same as Minimal API) +- `CreationResponse` is a built-in Wolverine type that sets the 201 status code and `Location` header +- `AcceptResponse` works similarly for 202 Accepted + +## Parameter Binding + +Parameter binding works very similarly between the two frameworks. Here's a comprehensive comparison: + +### Minimal API + +```csharp +app.MapGet("/api/orders", ( + [FromQuery] int page, + [FromQuery] int pageSize, + [FromHeader(Name = "X-Tenant")] string tenant, + [FromServices] IOrderRepository repo, + ClaimsPrincipal user, + CancellationToken ct) => +{ + // ... +}); +``` + +### Wolverine + +```csharp +[WolverineGet("/api/orders")] +public static Task> Get( + int page, // query string (inferred for simple types) + int pageSize, // query string (inferred) + [FromHeader(Name = "X-Tenant")] string tenant, // header — same attribute + IOrderRepository repo, // IoC service (inferred, no attribute needed) + ClaimsPrincipal user, // injected automatically + CancellationToken ct) // injected automatically +{ + // ... +} +``` + +**Key differences:** +- Wolverine infers query string binding for simple types without `[FromQuery]` +- Wolverine infers service injection without `[FromServices]` (though the attribute still works) +- `HttpContext`, `HttpRequest`, `HttpResponse`, `ClaimsPrincipal`, and `CancellationToken` are all injected automatically in both frameworks + +### Complex Query Objects + +**Minimal API (.NET 7+):** + +```csharp +app.MapGet("/api/orders", ([AsParameters] OrderQuery query, IOrderRepository repo) => { ... }); +``` + +**Wolverine (v3.12+):** + +```csharp +[WolverineGet("/api/orders")] +public static Task> Get( + [FromQuery] OrderQuery query, IOrderRepository repo) +{ + // query.Page, query.PageSize, etc. bound from query string +} +``` + +## Form Data and File Uploads + +### Minimal API + +```csharp +app.MapPost("/api/upload", async ([FromForm] string description, IFormFile file) => +{ + using var stream = file.OpenReadStream(); + // process file... + return Results.Ok(new { file.FileName, description }); +}); +``` + +### Wolverine + +```csharp +[WolverinePost("/api/upload")] +public static async Task Post( + [FromForm] string description, IFormFile file) +{ + using var stream = file.OpenReadStream(); + // process file... + return new { file.FileName, description }; +} +``` + +File upload binding is identical — `IFormFile` and `IFormFileCollection` work the same way. + +## Authentication and Authorization + +### Minimal API + +```csharp +app.MapGet("/api/admin/users", () => { ... }) + .RequireAuthorization("AdminPolicy"); + +app.MapGet("/api/public/health", () => "ok") + .AllowAnonymous(); +``` + +### Wolverine + +```csharp +[Authorize(Policy = "AdminPolicy")] +[WolverineGet("/api/admin/users")] +public static IEnumerable Get(IUserRepository repo) { ... } + +[AllowAnonymous] +[WolverineGet("/api/public/health")] +public static string Get() => "ok"; +``` + +To require authorization on all Wolverine endpoints globally: + +```csharp +app.MapWolverineEndpoints(opts => +{ + opts.RequireAuthorizeOnAll(); +}); +``` + +## Route Groups → IHttpPolicy + +### Minimal API + +```csharp +var api = app.MapGroup("/api") + .RequireAuthorization() + .AddEndpointFilter(); + +var orders = api.MapGroup("/orders"); +orders.MapGet("/", GetOrders.Handle); +orders.MapPost("/", CreateOrder.Handle); +``` + +### Wolverine + +Wolverine doesn't have route groups. Instead, use `IHttpPolicy` to apply cross-cutting concerns +to groups of endpoints by namespace, type, or any other criteria: + +```csharp +app.MapWolverineEndpoints(opts => +{ + // Apply middleware to endpoints in a namespace + opts.AddMiddleware(typeof(LoggingMiddleware), + chain => chain.Method.HandlerType.IsInNamespace("MyApp.Features.Orders")); +}); +``` + +See [Route Prefix Groups](https://github.com/JasperFx/wolverine/issues/2405) for future +route prefix support. + +## Endpoint Filters → Before/After Methods + +### Minimal API + +```csharp +app.MapPost("/api/orders", CreateOrder.Handle) + .AddEndpointFilter(async (context, next) => + { + var command = context.GetArgument(0); + if (string.IsNullOrWhiteSpace(command.ProductName)) + return Results.BadRequest("Product name is required"); + + return await next(context); + }); +``` + +### Wolverine + +```csharp +public static class CreateOrderEndpoint +{ + // "Validate" is a recognized Before method — runs before the handler + public static ProblemDetails Validate(CreateOrderCommand command) + { + if (string.IsNullOrWhiteSpace(command.ProductName)) + return new ProblemDetails + { + Detail = "Product name is required", + Status = 400 + }; + + return WolverineContinue.NoProblems; + } + + [WolverinePost("/api/orders")] + public static OrderConfirmation Post(CreateOrderCommand command) { ... } +} +``` + +For a comprehensive filter migration guide, see +[Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration). + +## Publishing Messages (The Wolverine Superpower) + +This is where Wolverine fundamentally differs from Minimal APIs — endpoints can trigger +asynchronous messaging with transactional guarantees. + +### Minimal API (Manual) + +```csharp +app.MapPost("/api/orders", async ( + CreateOrder command, + IOrderRepository repo, + IMessageBroker broker) => +{ + var order = new Order { ... }; + await repo.SaveAsync(order); + // Manual, non-transactional — if this fails, order is saved but event is lost + await broker.PublishAsync(new OrderCreated(order.Id)); + return Results.Created($"/api/orders/{order.Id}", order); +}); +``` + +### Wolverine (Cascading Messages) + +```csharp +public static class CreateOrderEndpoint +{ + [WolverinePost("/api/orders")] + public static (CreationResponse, OrderCreated) Post( + CreateOrder command, IDocumentSession session) + { + var order = new Order { Id = Guid.NewGuid(), ProductName = command.ProductName }; + session.Store(order); + + // First tuple item = HTTP response (201 Created) + // Second tuple item = cascading message, sent transactionally via outbox + return ( + new CreationResponse($"/api/orders/{order.Id}"), + new OrderCreated(order.Id) + ); + } +} +``` + +**Key differences:** +- Tuple return values cascade messages through Wolverine's messaging pipeline +- With transactional middleware, the message is sent via the outbox — guaranteed delivery +- No explicit `IMessageBus` injection needed for cascading +- Multiple cascading messages via additional tuple items or `OutgoingMessages` + +### Multiple Cascading Messages + +```csharp +[WolverinePost("/api/orders")] +public static (CreationResponse, OutgoingMessages) Post(CreateOrder command) +{ + var order = new Order { ... }; + var messages = new OutgoingMessages + { + new OrderCreated(order.Id), + new NotifyWarehouse(order.Id), + new SendConfirmationEmail(order.CustomerEmail) + }; + return (new CreationResponse($"/api/orders/{order.Id}"), messages); +} +``` + +## Delegate-to-Wolverine Shortcut + +If you want the absolute minimum conversion from Minimal API, Wolverine provides shortcut +methods that wire a Minimal API route directly to a Wolverine message handler: + +```csharp +// Instead of: app.MapPost("/orders", (CreateOrder cmd, IMessageBus bus) => bus.InvokeAsync(cmd)); +app.MapPostToWolverine("/orders"); + +// With a response type: +app.MapPostToWolverine("/orders"); + +// Also available for PUT and DELETE: +app.MapPutToWolverine("/orders"); +app.MapDeleteToWolverine("/orders/{id}"); +``` + +These are optimized Minimal API endpoints that delegate to Wolverine's handler pipeline — +a quick way to integrate Wolverine into an existing Minimal API application without rewriting +endpoints. + +## OpenAPI Metadata + +### Minimal API + +```csharp +app.MapGet("/api/orders/{id}", GetOrder.Handle) + .WithTags("Orders") + .WithDescription("Get an order by ID") + .Produces(200) + .Produces(404) + .WithOpenApi(); +``` + +### Wolverine + +```csharp +[Tags("Orders")] +[WolverineGet("/api/orders/{id}", OperationId = "GetOrder")] +[ProducesResponseType(typeof(Order), 200)] +[ProducesResponseType(404)] +public static Order Get([Entity] Order order) => order; +``` + +Wolverine also generates sensible OpenAPI defaults from the method signature — JSON content types, +200/404/500 status codes, and parameter metadata are inferred automatically. + +## Registration + +### Minimal API + +```csharp +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/orders", GetOrders.Handle); +app.MapPost("/orders", CreateOrder.Handle); +// ... register each endpoint manually +``` + +### Wolverine + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseWolverine(); + +var app = builder.Build(); +app.MapWolverineEndpoints(); +// All endpoints discovered automatically from [WolverineGet], [WolverinePost], etc. +``` + +Wolverine discovers endpoints by scanning assemblies for methods decorated with Wolverine +route attributes. No manual registration needed. + +## Summary + +| Concept | Minimal API | Wolverine.HTTP | +|---------|------------|----------------| +| **Declaration** | `app.MapGet("/path", handler)` | `[WolverineGet("/path")]` on a method | +| **Body binding** | First complex param (inferred) | First complex param (inferred) | +| **Query binding** | `[FromQuery]` or inferred | Inferred for simple types | +| **Service injection** | `[FromServices]` or inferred | Inferred (attribute optional) | +| **Header binding** | `[FromHeader]` | `[FromHeader]` | +| **Authorization** | `.RequireAuthorization()` | `[Authorize]` | +| **Validation** | `IEndpointFilter` | `Validate` method or FluentValidation | +| **Route groups** | `app.MapGroup()` | `IHttpPolicy` with namespace filtering | +| **201 Created** | `Results.Created(url, body)` | Return `CreationResponse` | +| **Cascading messages** | Manual via message broker | Tuple returns or `OutgoingMessages` | +| **Registration** | Manual per-endpoint | Automatic assembly scanning | + +## Further Reading + +- [Wolverine.HTTP Endpoints](/guide/http/endpoints) — full endpoint reference +- [Publishing Messages from HTTP](/guide/http/messaging) — cascading messages guide +- [Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration) — filter migration +- [Railway Programming](/tutorials/railway-programming) — validation and loading patterns diff --git a/docs/tutorials/from-mvc.md b/docs/tutorials/from-mvc.md new file mode 100644 index 000000000..b88881f18 --- /dev/null +++ b/docs/tutorials/from-mvc.md @@ -0,0 +1,516 @@ +# Migrating from MVC Controllers to Wolverine.HTTP + +This tutorial provides side-by-side conversions between ASP.NET Core MVC/Web API controllers and +their Wolverine.HTTP equivalents. If you've been building APIs with `ControllerBase` and +`[ApiController]`, this will show you how each pattern maps to Wolverine. + +::: tip +For filter and middleware migration specifically, see +[Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration). +::: + +## Basic CRUD Controller + +### MVC Controller + +```csharp +[ApiController] +[Route("api/[controller]")] +public class OrdersController : ControllerBase +{ + private readonly IOrderRepository _repo; + + public OrdersController(IOrderRepository repo) => _repo = repo; + + [HttpGet("{id}")] + public async Task> GetById(int id) + { + var order = await _repo.GetByIdAsync(id); + if (order == null) return NotFound(); + return Ok(order); + } + + [HttpPost] + public async Task> Create(CreateOrderRequest request) + { + var order = new Order { ProductName = request.ProductName }; + await _repo.SaveAsync(order); + return CreatedAtAction(nameof(GetById), new { id = order.Id }, order); + } + + [HttpPut("{id}")] + public async Task Update(int id, UpdateOrderRequest request) + { + var order = await _repo.GetByIdAsync(id); + if (order == null) return NotFound(); + order.ProductName = request.ProductName; + await _repo.SaveAsync(order); + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var order = await _repo.GetByIdAsync(id); + if (order == null) return NotFound(); + await _repo.DeleteAsync(id); + return NoContent(); + } +} +``` + +### Wolverine Endpoints + +In Wolverine, there is no controller base class. Each endpoint is a plain method on a plain class. +You can organize endpoints however you like — one class per endpoint, or group related endpoints +in a single class: + +```csharp +public static class OrderEndpoints +{ + [WolverineGet("/api/orders/{id}")] + public static Order GetById([Entity] Order order) => order; + + [WolverinePost("/api/orders")] + public static async Task Create( + CreateOrderRequest request, IOrderRepository repo) + { + var order = new Order { ProductName = request.ProductName }; + await repo.SaveAsync(order); + return new CreationResponse($"/api/orders/{order.Id}"); + } + + [WolverineDelete("/api/orders/{id}")] + public static Task Delete(int id, IOrderRepository repo) + { + return repo.DeleteAsync(id); + // this will set a 204 HTTP status code + } +} + +public static class UpdateOrderEndpoint +{ + // This could be further reduced by using the [Entity] attribute + // if you'll also drop the custom repository wrappers:) + public static async Task LoadAsync(int id, IOrderRepository repo) + => await repo.GetByIdAsync(id); + + [WolverinePut("/api/orders/{id}")] + public static async Task Put( + UpdateOrderRequest request, + [Required] Order? order, + IOrderRepository repo) + { + order!.ProductName = request.ProductName; + await repo.SaveAsync(order); + return 204; + } +} +``` + +**Key differences:** +- No `ControllerBase` inheritance, no constructor injection — services are method parameters +- `[Entity]` handles loading + 404 in one shot (replaces the manual load-and-check pattern) +- `Load`/`LoadAsync` methods on the class replace the "fetch then check null" boilerplate +- `[Required]` on a nullable parameter returns 404 automatically if the loaded value is null +- `CreationResponse` replaces `CreatedAtAction()` — sets 201 + Location header +- Returning `int` sets the HTTP status code (e.g. `return 204;`) + +## Dependency Injection + +### MVC (Constructor Injection) + +```csharp +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IProductService _products; + private readonly ILogger _logger; + private readonly IValidator _validator; + + public ProductsController( + IProductService products, + ILogger logger, + IValidator validator) + { + _products = products; + _logger = logger; + _validator = validator; + } + + [HttpPost] + public async Task> Create(CreateProduct command) + { + var result = await _validator.ValidateAsync(command); + if (!result.IsValid) return BadRequest(result.Errors); + + var product = await _products.CreateAsync(command); + _logger.LogInformation("Created product {Id}", product.Id); + return CreatedAtAction(nameof(Get), new { id = product.Id }, product); + } +} +``` + +### Wolverine (Method Parameter Injection) + +```csharp +public static class CreateProductEndpoint +{ + [WolverinePost("/api/products")] + public static async Task Post( + CreateProduct command, // deserialized from request body + IProductService products, // injected from IoC + ILogger logger) // injected from IoC + { + var product = await products.CreateAsync(command); + logger.LogInformation("Created product {Id}", product.Id); + return new CreationResponse($"/api/products/{product.Id}"); + } +} +``` + +**Key differences:** +- No constructor — each method declares exactly the dependencies it needs +- No field assignments, no `_` prefix convention +- Validation is handled via FluentValidation middleware or `Validate` methods, not manually +- Services are resolved per-method, not per-controller-instance + +## Model Binding + +### MVC + +```csharp +[HttpGet] +public IActionResult Search( + [FromQuery] string? name, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20, + [FromHeader(Name = "X-Correlation-Id")] string? correlationId, + [FromRoute] string category) +{ + // ... +} +``` + +### Wolverine + +```csharp +[WolverineGet("/api/products/{category}")] +public static IEnumerable Search( + string category, // route parameter (matched by name) + string? name, // query string (inferred for simple types) + int page, // query string (default is 0, not configurable inline) + int pageSize, // query string + [FromHeader(Name = "X-Correlation-Id")] string? correlationId) +{ + // ... +} +``` + +**Binding rules comparison:** + +| Source | MVC | Wolverine | +|--------|-----|-----------| +| Route | `[FromRoute]` or inferred | Inferred by parameter name matching route template | +| Query string | `[FromQuery]` or inferred for simple types | Inferred for simple types | +| Body | `[FromBody]` (or inferred with `[ApiController]`) | First complex type parameter (inferred) | +| Header | `[FromHeader]` | `[FromHeader]` | +| Service | `[FromServices]` | Inferred (no attribute needed) | +| Form | `[FromForm]` | `[FromForm]` | + +## Request Body and [ApiController] + +### MVC with [ApiController] + +```csharp +[ApiController] // enables automatic model validation and body binding +[Route("api/[controller]")] +public class OrdersController : ControllerBase +{ + [HttpPost] + public ActionResult Create(CreateOrder command) + { + // [ApiController] automatically: + // 1. Binds 'command' from request body + // 2. Validates with DataAnnotations + // 3. Returns 400 if ModelState is invalid + } +} +``` + +### Wolverine + +```csharp +public static class CreateOrderEndpoint +{ + [WolverinePost("/api/orders")] + public static Order Post(CreateOrder command) + { + // Wolverine automatically deserializes 'command' from JSON body + // (first complex type parameter is always the body) + } +} +``` + +For automatic validation, add FluentValidation or DataAnnotations middleware: + +```csharp +// In startup +app.MapWolverineEndpoints(opts => +{ + opts.UseFluentValidationProblemDetailMiddleware(); + // or + opts.UseDataAnnotationsValidationProblemDetailMiddleware(); +}); +``` + +## ActionResult → Return Types + +MVC uses `ActionResult` extensively. Wolverine uses simpler return types: + +### MVC + +```csharp +[HttpGet("{id}")] +public async Task> Get(int id) +{ + var order = await _repo.FindAsync(id); + if (order == null) return NotFound(); + return Ok(order); +} + +[HttpPost] +public async Task> Create(CreateOrder command) +{ + var order = await _service.CreateAsync(command); + return CreatedAtAction(nameof(Get), new { id = order.Id }, order); +} + +[HttpDelete("{id}")] +public async Task Delete(int id) +{ + await _repo.DeleteAsync(id); + return NoContent(); +} +``` + +### Wolverine + +```csharp +// Simple GET — return the object directly, Wolverine serializes to JSON (200) +[WolverineGet("/api/orders/{id}")] +public static Order Get([Entity] Order order) => order; + +// POST with 201 — use CreationResponse +[WolverinePost("/api/orders")] +public static CreationResponse Create(CreateOrder command) +{ + var order = /* create */; + return new CreationResponse($"/api/orders/{order.Id}"); +} + +// DELETE with 204 — return status code as int +[WolverineDelete("/api/orders/{id}")] +public static int Delete(int id) +{ + /* delete */ + return 204; +} + +// Need full control? IResult works too +[WolverineGet("/api/orders/{id}/details")] +public static async Task GetDetails(int id, IOrderRepository repo) +{ + var order = await repo.FindAsync(id); + return order is not null ? Results.Ok(order) : Results.NotFound(); +} +``` + +**Return type mapping:** + +| MVC | Wolverine | +|-----|-----------| +| `Ok(value)` | Return the value directly | +| `NotFound()` | Use `[Entity]` (auto 404) or return `Results.NotFound()` | +| `CreatedAtAction(...)` | Return `CreationResponse(url)` | +| `Accepted(...)` | Return `AcceptResponse(url)` | +| `NoContent()` | Return `204` (int) | +| `BadRequest(...)` | Return `ProblemDetails` from a `Validate` method | +| `StatusCode(n)` | Return `n` (int) | +| Any `IActionResult` | Return `IResult` (Minimal API result type) | + +## ModelState Validation → Validate Methods + +### MVC + +```csharp +[HttpPost] +public ActionResult Create(CreateOrder command) +{ + if (!ModelState.IsValid) + return BadRequest(ModelState); + + // Or with [ApiController], this check happens automatically +} +``` + +### Wolverine + +```csharp +public static class CreateOrderEndpoint +{ + public static ProblemDetails Validate(CreateOrder command) + { + if (string.IsNullOrWhiteSpace(command.ProductName)) + return new ProblemDetails + { + Detail = "Product name is required", + Status = 400 + }; + + return WolverineContinue.NoProblems; + } + + [WolverinePost("/api/orders")] + public static Order Post(CreateOrder command) { ... } +} +``` + +Or use FluentValidation for automatic validation (closest to `[ApiController]` behavior): + +```csharp +// Register once at startup +app.MapWolverineEndpoints(opts => +{ + opts.UseFluentValidationProblemDetailMiddleware(); +}); + +// Validator class — discovered and applied automatically +public class CreateOrderValidator : AbstractValidator +{ + public CreateOrderValidator() + { + RuleFor(x => x.ProductName).NotEmpty(); + RuleFor(x => x.Quantity).GreaterThan(0); + } +} + +// Endpoint — no validation code needed, FluentValidation runs before the handler +[WolverinePost("/api/orders")] +public static Order Post(CreateOrder command) { ... } +``` + +## Controller Filters → Wolverine Middleware + +### MVC (Attribute Filters on Controller) + +```csharp +[Authorize] +[ServiceFilter(typeof(AuditFilter))] +[ApiController] +[Route("api/[controller]")] +public class AdminController : ControllerBase +{ + [HttpPost("reset")] + public IActionResult Reset() { ... } + + [HttpPost("purge")] + public IActionResult Purge() { ... } +} +``` + +### Wolverine (Middleware on Class) + +```csharp +[Authorize] +[Middleware(typeof(AuditMiddleware))] +public static class AdminEndpoints +{ + [WolverinePost("/api/admin/reset")] + public static int Reset() { /* ... */ return 200; } + + [WolverinePost("/api/admin/purge")] + public static int Purge() { /* ... */ return 200; } +} +``` + +For comprehensive filter migration examples, see +[Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration). + +## Cascading Messages (No MVC Equivalent) + +This is a capability MVC controllers simply don't have. Wolverine endpoints can trigger +asynchronous messages as part of the HTTP response, with transactional outbox guarantees: + +```csharp +public static class PlaceOrderEndpoint +{ + [WolverinePost("/api/orders")] + public static (CreationResponse, OrderPlaced, NotifyWarehouse) Post( + PlaceOrder command, IDocumentSession session) + { + var order = new Order { ... }; + session.Store(order); + + return ( + new CreationResponse($"/api/orders/{order.Id}"), // HTTP response (201) + new OrderPlaced(order.Id), // message → handler + new NotifyWarehouse(order.Id, order.Items) // message → handler + ); + } +} +``` + +The `OrderPlaced` and `NotifyWarehouse` messages are sent through Wolverine's messaging pipeline +after the HTTP response is committed. With transactional middleware enabled, the messages are +persisted via the outbox in the same database transaction as the order — guaranteed delivery +even if the process crashes. + +## Registration + +### MVC + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddControllers(); + +var app = builder.Build(); +app.MapControllers(); // discovers controllers by convention +``` + +### Wolverine + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseWolverine(); + +var app = builder.Build(); +app.MapWolverineEndpoints(); // discovers endpoints by attribute scanning +``` + +Both frameworks use automatic discovery. MVC finds classes inheriting `ControllerBase`; +Wolverine finds methods decorated with `[WolverineGet]`, `[WolverinePost]`, etc. + +## Summary + +| Concept | MVC Controller | Wolverine.HTTP | +|---------|---------------|----------------| +| **Base class** | `ControllerBase` required | No base class — plain static classes | +| **DI pattern** | Constructor injection | Method parameter injection | +| **Route prefix** | `[Route("api/[controller]")]` | Full route in attribute | +| **Body binding** | `[FromBody]` or `[ApiController]` inferred | First complex param (always inferred) | +| **Validation** | ModelState + `[ApiController]` | `Validate` methods or FluentValidation middleware | +| **Responses** | `ActionResult`, `IActionResult` | Direct return, `IResult`, `CreationResponse`, or `int` | +| **Filters** | `IActionFilter`, `IExceptionFilter`, etc. | `Before`/`After`/`Finally` methods | +| **Entity loading** | Manual in action or filter | `[Entity]` attribute or `Load`/`LoadAsync` method | +| **Async messaging** | Not built-in | Tuple returns, `OutgoingMessages` | +| **Registration** | `AddControllers()` + `MapControllers()` | `UseWolverine()` + `MapWolverineEndpoints()` | + +## Further Reading + +- [Wolverine.HTTP Endpoints](/guide/http/endpoints) — full endpoint reference +- [Wolverine for MediatR Users](/introduction/from-mediatr) — if you're using MediatR with MVC +- [Migrating from MVC/Minimal API Filters](/tutorials/middleware-migration) — filter migration +- [Railway Programming](/tutorials/railway-programming) — validation and loading patterns +- [Publishing Messages from HTTP](/guide/http/messaging) — cascading messages guide diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index feb9e5e35..254221d86 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -14,3 +14,6 @@ | [Dead Letter Queues](./dead-letter-queues)| Understand how dead letter queueing works in Wolverine and how to manage message failures | | [Idempotency in Messaging](./idempotency) | Find out how best to use Wolverine's built in support for messaging idempotency | | [Multi-Tenancy](./multi-tenancy) | A holistic guide to multi-tenancy across HTTP, messaging, and persistence in Wolverine | +| [Migrating from Minimal APIs](./from-minimal-api) | Side-by-side conversions from ASP.NET Core Minimal API endpoints to Wolverine.HTTP | +| [Migrating from MVC Controllers](./from-mvc) | Side-by-side conversions from MVC/Web API controllers to Wolverine.HTTP | +| [Migrating from MVC/Minimal API Filters](./middleware-migration) | Map MVC filters and Minimal API endpoint filters to Wolverine.HTTP middleware | diff --git a/docs/tutorials/middleware-migration.md b/docs/tutorials/middleware-migration.md new file mode 100644 index 000000000..41acf6916 --- /dev/null +++ b/docs/tutorials/middleware-migration.md @@ -0,0 +1,632 @@ +# Migrating from MVC Filters and Minimal API Endpoint Filters + +::: tip +See our guide on [HTTP Middleware](/guide/http/middleware) for more information. +::: + +If you're coming from ASP.NET Core MVC or Minimal APIs, you're used to filters like `IActionFilter`, +`IEndpointFilter`, `IResultFilter`, and friends. Wolverine.HTTP replaces all of these with a single, +convention-based middleware system that is compile-time code generated, requires no interface ceremony, +and gives you the same (and often more) control. + +This tutorial maps the filter concepts you already know to their idiomatic Wolverine equivalents. + +## The Core Difference + +In MVC and Minimal APIs, filters are **runtime pipeline delegates** that wrap your endpoint. You implement +interfaces, register them globally or per-endpoint, and they execute via delegate chains at runtime. + +Wolverine takes a fundamentally different approach: middleware is **compiled directly into generated C# code**. +When Wolverine bootstraps, it detects your middleware methods by naming convention and weaves them into the +generated handler source code. The result is: + +- **Zero allocation overhead** — no delegate chains or middleware pipeline objects +- **Clean stack traces** — no nested middleware layers obscuring where an error occurred +- **Compile-time validation** — middleware parameter mismatches are caught at startup, not at request time + +## Quick Reference + +| MVC / Minimal API | Wolverine Equivalent | +|-------------------|---------------------| +| `IActionFilter.OnActionExecuting` | `Before` / `BeforeAsync` method | +| `IEndpointFilter` | `Before` / `BeforeAsync` method | +| `IAuthorizationFilter` | `Before` returning `IResult` (e.g. `Results.Unauthorized()`) | +| `IResourceFilter.OnResourceExecuting` | `Before` with early `IResult` return | +| `IActionFilter.OnActionExecuted` | `After` / `AfterAsync` method | +| `IResultFilter` | `After` / `AfterAsync` method | +| `IExceptionFilter` | `Finally` / `FinallyAsync` method | +| `[TypeFilter]` / `[ServiceFilter]` | `[Middleware(typeof(...))]` attribute | +| Global filters (`MvcOptions.Filters`) | `IHttpPolicy` | +| `AddEndpointFilter()` on route groups | `IHttpPolicy` with namespace/type filtering | +| Filter ordering (`Order` property) | Insertion position in `chain.Middleware` list | + +## Before Methods: Replacing IActionFilter and IEndpointFilter + +### MVC IActionFilter + +In MVC, you'd implement `IActionFilter` to run logic before an action: + +```csharp +// MVC approach +public class LoggingFilter : IActionFilter +{ + private readonly ILogger _logger; + + public LoggingFilter(ILogger logger) => _logger = logger; + + public void OnActionExecuting(ActionExecutingContext context) + { + _logger.LogInformation("Executing {Action}", context.ActionDescriptor.DisplayName); + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} + +// Applied via attribute +[ServiceFilter(typeof(LoggingFilter))] +public IActionResult GetOrder(int id) { ... } +``` + +### Minimal API IEndpointFilter + +In Minimal APIs, you'd use `IEndpointFilter`: + +```csharp +// Minimal API approach +public class LoggingFilter : IEndpointFilter +{ + public async ValueTask InvokeAsync( + EndpointFilterInvocationContext context, + EndpointFilterDelegate next) + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + logger.LogInformation("Executing endpoint"); + + var result = await next(context); + return result; + } +} + +// Applied to an endpoint +app.MapGet("/orders/{id}", (int id) => ...) + .AddEndpointFilter(); +``` + +### Wolverine Equivalent + +In Wolverine, middleware is just a class with methods following naming conventions. No interfaces: + +```csharp +// Wolverine approach — just a class with a Before method +public static class LoggingMiddleware +{ + // Dependencies are injected as method parameters — no constructor needed + public static void Before(HttpContext context, ILogger logger) + { + logger.LogInformation("Executing {Path}", context.Request.Path); + } +} +``` + +Apply it to a specific endpoint with `[Middleware]`: + +```csharp +[Middleware(typeof(LoggingMiddleware))] +[WolverineGet("/orders/{id}")] +public static Order Get([Entity] Order order) => order; +``` + +Or put the `Before` method directly on the endpoint class — no separate middleware class needed: + +```csharp +public static class GetOrderEndpoint +{ + // This runs before the handler automatically — discovered by naming convention + public static void Before(HttpContext context, ILogger logger) + { + logger.LogInformation("Executing {Path}", context.Request.Path); + } + + [WolverineGet("/orders/{id}")] + public static Order Get([Entity] Order order) => order; +} +``` + +::: tip +All of the following method names are recognized as "before" middleware: +`Before`, `BeforeAsync`, `Load`, `LoadAsync`, `Validate`, `ValidateAsync` + +See [Compound Handlers](https://wolverinefx.io/guide/handlers/#compound-handlers) for more information about +how the conventional method names behave in both HTTP endpoints and Wolverine message handlers. +::: + +## Short-Circuiting: Replacing IAuthorizationFilter and IResourceFilter + +A major use of filters is short-circuiting — stopping the request before the handler runs. +In MVC, you'd set `context.Result` in `OnActionExecuting`. In Minimal APIs, you'd return early +from `IEndpointFilter` without calling `next()`. + +### MVC Authorization Filter + +```csharp +// MVC approach +public class ApiKeyFilter : IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.Request.Headers.TryGetValue("X-Api-Key", out var key) + || key != "secret") + { + context.Result = new UnauthorizedResult(); + } + } +} +``` + +### Wolverine Equivalent + +In Wolverine, return an `IResult` from a `Before` method. If the return value is anything +other than `WolverineContinue.Result()`, Wolverine writes that result to the response and +stops processing: + +```csharp +public static class ApiKeyMiddleware +{ + public static IResult Before(HttpContext context) + { + if (!context.Request.Headers.TryGetValue("X-Api-Key", out var key) + || key != "secret") + { + // Returning any IResult other than WolverineContinue stops processing + return Results.Unauthorized(); + } + + // This tells Wolverine to keep going to the handler + return WolverineContinue.Result(); + } +} +``` + +### Using ProblemDetails for Validation + +For validation scenarios, you can return `ProblemDetails` instead of `IResult`: + +```csharp +public static class CreateOrderEndpoint +{ + // Naming convention: "Validate" is recognized as a Before method + public static ProblemDetails Validate(CreateOrderRequest request) + { + if (string.IsNullOrWhiteSpace(request.ProductName)) + { + return new ProblemDetails + { + Detail = "Product name is required", + Status = 400 + }; + } + + // WolverineContinue.NoProblems signals "validation passed, keep going" + return WolverineContinue.NoProblems; + } + + [WolverinePost("/orders")] + public static OrderConfirmation Post(CreateOrderRequest request) + { + // Only reached if Validate returned NoProblems + return new OrderConfirmation(Guid.NewGuid()); + } +} +``` + +### Using Nullable IResult + +You can also use a nullable `IResult` return type — `null` means "continue": + +```csharp +public static class AuthMiddleware +{ + // Returning null means "keep going" + // Returning a non-null IResult means "stop and write this response" + public static UnauthorizedHttpResult? Before(ClaimsPrincipal user) + { + return user.Identity?.IsAuthenticated == true + ? null + : TypedResults.Unauthorized(); + } +} +``` + +## Data Loading: Replacing IResourceFilter + +A common MVC pattern is using `IResourceFilter` to load data before model binding: + +```csharp +// MVC approach +public class LoadOrderFilter : IResourceFilter +{ + private readonly IOrderRepository _repo; + public LoadOrderFilter(IOrderRepository repo) => _repo = repo; + + public void OnResourceExecuting(ResourceExecutingContext context) + { + var id = (int)context.RouteData.Values["id"]!; + var order = _repo.GetById(id); + if (order == null) + { + context.Result = new NotFoundResult(); + return; + } + context.HttpContext.Items["order"] = order; + } + + public void OnResourceExecuted(ResourceExecutedContext context) { } +} +``` + +### Wolverine Equivalent + +Use a `Load` or `LoadAsync` method. The return value is passed as a parameter to the handler: + +```csharp +public static class UpdateOrderEndpoint +{ + // "LoadAsync" is a recognized Before method name + // Its return value is passed to the handler as a parameter + public static Task LoadAsync(int id, IDocumentSession session) + => session.LoadAsync(id); + + [WolverinePut("/orders/{id}")] + public static IMartenOp Put( + UpdateOrderRequest request, + [Required] Order? order) // [Required] returns 404 automatically if null + { + order!.Name = request.Name; + return MartenOps.Store(order); + } +} +``` + +The `[Required]` attribute on a nullable parameter tells Wolverine to return a 404 if the loaded +value is `null` — replacing the manual null check in the MVC filter. + +To go farther into Wolverine, you can simplify that even more with our [persistence helpers](https://wolverinefx.io/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters) +and simplify the code above even more: + +```csharp +public static class UpdateOrderEndpoint +{ + [WolverinePut("/orders/{id}")] + public static IMartenOp Put( + UpdateOrderRequest request, + [Entity(Required = true)] Order? order) // returns 404 automatically if null + { + order!.Name = request.Name; + return MartenOps.Store(order); + } +} +``` + +## After Methods: Replacing IResultFilter + +### MVC IResultFilter + +```csharp +// MVC approach +public class AddHeaderFilter : IResultFilter +{ + public void OnResultExecuting(ResultExecutingContext context) + { + context.HttpContext.Response.Headers["X-Custom"] = "value"; + } + public void OnResultExecuted(ResultExecutedContext context) { } +} +``` + +### Wolverine Equivalent + +Use an `After` method: + +```csharp +public static class CustomHeaderMiddleware +{ + public static void After(HttpContext context) + { + context.Response.Headers["X-Custom"] = "value"; + } +} +``` + +## Finally Methods: Replacing IExceptionFilter + +### MVC IExceptionFilter + +```csharp +// MVC approach +public class ErrorHandlingFilter : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + if (context.Exception is NotFoundException) + { + context.Result = new NotFoundResult(); + context.ExceptionHandled = true; + } + } +} +``` + +### Wolverine Equivalent + +Use a `Finally` method. It runs in a `try/finally` block, guaranteeing execution even when +exceptions occur: + +```csharp +public class StopwatchMiddleware +{ + private readonly Stopwatch _stopwatch = new(); + + public void Before() + { + _stopwatch.Start(); + } + + public void Finally(ILogger logger, HttpContext context) + { + _stopwatch.Stop(); + logger.LogDebug("Request to {Path} took {Duration}ms", + context.Request.Path, _stopwatch.ElapsedMilliseconds); + } +} +``` + +::: warning +Note that `Finally` methods do not receive the exception — they are cleanup hooks, not exception +handlers. For exception-to-response mapping, see [issue #2410](https://github.com/JasperFx/wolverine/issues/2410) +which is tracking a dedicated exception handling convention. +::: + +## Applying Middleware Per-Endpoint + +Wolverine provides several ways to apply middleware to specific endpoints, from most targeted to broadest: + +### 1. Inline on the Endpoint Class + +Put `Before`/`After`/`Finally` methods directly on the endpoint class. They apply only to that endpoint: + +```csharp +public static class SecureOrderEndpoint +{ + // Only applies to this endpoint + public static IResult Before(ClaimsPrincipal user) + { + return user.IsInRole("OrderAdmin") + ? WolverineContinue.Result() + : Results.Forbid(); + } + + [WolverinePost("/orders/cancel/{id}")] + public static void Post(CancelOrder command) { ... } +} +``` + +### 2. The `[Middleware]` Attribute + +Apply a middleware class to a single endpoint method or an entire endpoint class: + +```csharp +// On a single method +public static class OrderEndpoints +{ + [Middleware(typeof(ApiKeyMiddleware))] + [WolverineDelete("/orders/{id}")] + public static void Delete(int id) { ... } + + // This endpoint does NOT have the middleware + [WolverineGet("/orders/{id}")] + public static Order Get([Entity] Order order) => order; +} + +// On the entire class — applies to all endpoints in the class +[Middleware(typeof(ApiKeyMiddleware))] +public static class AdminEndpoints +{ + [WolverinePost("/admin/reset")] + public static void Reset() { ... } + + [WolverinePost("/admin/purge")] + public static void Purge() { ... } +} +``` + +You can also apply multiple middleware classes and control scoping: + +```csharp +[Middleware(typeof(LoggingMiddleware), typeof(AuthMiddleware))] +[WolverinePost("/orders")] +public static OrderConfirmation Post(CreateOrderRequest request) { ... } +``` + +### 3. The `Configure(HttpChain)` Method + +For programmatic control over a specific endpoint's middleware pipeline, add a static +`Configure` method to the endpoint class: + +```csharp +public class TimedEndpoint +{ + public static void Configure(HttpChain chain) + { + // Add middleware before the handler + chain.AddMiddleware(x => x.Before()); + + // Add a postprocessor after the handler + chain.AddPostprocessor(x => x.Finally(null!, null!)); + + // You can also manipulate OpenAPI metadata here + chain.Metadata.Produces(503); + } + + [WolverineGet("/timed")] + public static string Get() => "how long did I take?"; +} +``` + +The `AddMiddleware()` and `AddPostprocessor()` extension methods accept a lambda pointing +to the middleware method. You can also use the non-generic overload with a type and method name: + +```csharp +chain.AddMiddleware(typeof(StopwatchMiddleware), nameof(StopwatchMiddleware.Before)); +``` + +This gives you full control over ordering — you can also directly manipulate `chain.Middleware` +and `chain.Postprocessors` as lists, using `Insert()` to place middleware at specific positions. + +### 4. `IHttpPolicy` for Groups of Endpoints + +For applying middleware to multiple endpoints based on criteria (namespace, type, dependencies), +implement `IHttpPolicy`: + +```csharp +// Apply audit logging to all endpoints in a specific namespace +public class AuditLoggingPolicy : IHttpPolicy +{ + public void Apply(IReadOnlyList chains, GenerationRules rules, + IServiceContainer container) + { + foreach (var chain in chains) + { + // Filter by namespace + if (chain.Method.HandlerType.IsInNamespace("MyApp.Features.Admin")) + { + chain.AddMiddleware(x => x.Before(null!, null!)); + } + } + } +} + +// Register the policy +app.MapWolverineEndpoints(opts => +{ + opts.AddPolicy(); +}); +``` + +This is the Wolverine equivalent of applying `AddEndpointFilter()` to a Minimal API route group +or registering global MVC filters with type-based filtering. + +**You can also filter based on service dependencies:** + +```csharp +public class TrainerLoadingPolicy : IHttpPolicy +{ + public void Apply(IReadOnlyList chains, GenerationRules rules, + IServiceContainer container) + { + foreach (var chain in chains) + { + // Only apply to endpoints that depend on the Trainer type + var dependencies = chain.ServiceDependencies(container, Type.EmptyTypes); + if (dependencies.Contains(typeof(Trainer))) + { + chain.AddMiddleware(x => x.LoadAsync(null!, null!)); + } + } + } +} +``` + +## Middleware with Tuple Returns + +A powerful Wolverine pattern with no MVC equivalent: middleware can return **tuples** that +both provide data to the handler and control flow: + +```csharp +public static class UserMiddleware +{ + // Returns both a UserId for the handler AND ProblemDetails for short-circuiting + public static (UserId, ProblemDetails) Load(ClaimsPrincipal principal) + { + var claim = principal.FindFirst("sub"); + if (claim != null && Guid.TryParse(claim.Value, out var id)) + { + return (new UserId(id), WolverineContinue.NoProblems); + } + + return (new UserId(Guid.Empty), new ProblemDetails + { + Detail = "Valid user identity required", + Status = 401 + }); + } +} +``` + +The `UserId` value is injected into the handler method, and the `ProblemDetails` controls whether +processing continues. This combines data loading and validation in a single middleware method. + +## Global Middleware vs. MVC Global Filters + +### MVC Global Filters + +```csharp +// MVC approach +builder.Services.AddControllers(options => +{ + options.Filters.Add(); + options.Filters.Add(); +}); +``` + +### Wolverine Equivalent + +```csharp +app.MapWolverineEndpoints(opts => +{ + // Apply middleware to ALL Wolverine endpoints + opts.AddMiddleware(typeof(LoggingMiddleware)); + + // Or with a filter predicate + opts.AddMiddleware(typeof(AuditMiddleware), + chain => chain.Method.HandlerType.IsInNamespace("MyApp.Admin")); + + // Or use a full policy for complex logic + opts.AddPolicy(); +}); +``` + +## Summary: Why Wolverine's Approach is Different + +| Concern | MVC / Minimal API | Wolverine | +|---------|-------------------|-----------| +| **Definition** | Implement interfaces (`IActionFilter`, `IEndpointFilter`) | Write methods with conventional names (`Before`, `After`, `Finally`) | +| **DI** | Constructor injection | Method parameter injection — no constructors needed | +| **Registration** | Attributes, global config, or `AddEndpointFilter()` | Inline methods, `[Middleware]`, `Configure(HttpChain)`, or `IHttpPolicy` | +| **Short-circuit** | Set `context.Result` or return early from `next()` | Return `IResult`, `ProblemDetails`, or nullable types | +| **Runtime cost** | Delegate chains with allocations | Compiled directly into generated source code — zero overhead | +| **Data passing** | `HttpContext.Items` dictionary or `context.Arguments` | Method return values automatically injected as handler parameters | +| **Validation** | Runtime errors if filter has wrong dependencies | Startup errors if middleware parameters can't be resolved | + +The key insight is that Wolverine middleware is **just methods** — no interfaces, no base classes, +no ceremony. The framework discovers them by naming convention and compiles them directly into the +request handling code. + +## One-Off Middleware with [WolverineBefore] / [WolverineAfter] + +If you have a one-off cross-cutting concern that doesn't warrant a separate middleware class — the +equivalent of slapping a single `IActionFilter` on one controller action — consider using Wolverine's +[Railway Programming](/tutorials/railway-programming) patterns instead. The `Before`, `Validate`, and +`Load` methods directly on your endpoint class serve the same purpose with less ceremony than creating +a dedicated middleware type. + +For reusable middleware that lives in a separate class and needs to be discoverable across endpoints, +Wolverine provides `[WolverineBefore]` and `[WolverineAfter]` attributes. See the +[Wolverine.HTTP Middleware](/guide/http/middleware) reference for details on these and other +advanced middleware patterns. + +## Further Reading + +- [Wolverine.HTTP Middleware](/guide/http/middleware) — full reference documentation +- [Wolverine.HTTP Policies](/guide/http/policies) — `IHttpPolicy` reference +- [Handler Middleware](/guide/handlers/middleware) — middleware for message handlers (same conventions) +- [Railway Programming with Wolverine](/tutorials/railway-programming) — validation and data loading patterns diff --git a/src/Wolverine/Configuration/IChain.cs b/src/Wolverine/Configuration/IChain.cs index 8cdfa2fe6..eeef40e00 100644 --- a/src/Wolverine/Configuration/IChain.cs +++ b/src/Wolverine/Configuration/IChain.cs @@ -1,3 +1,4 @@ +using System.Linq.Expressions; using System.Reflection; using JasperFx; using JasperFx.CodeGeneration; @@ -29,6 +30,53 @@ public static bool MatchesScope(this IChain chain, MethodInfo method) } } +public static class ChainMiddlewareExtensions +{ + /// + /// Add a middleware method call to this chain's middleware pipeline + /// + /// The chain to add middleware to + /// Expression pointing to the middleware method + /// The middleware class type + public static void AddMiddleware(this IChain chain, Expression> method) + { + chain.Middleware.Add(new MethodCall(typeof(T), ReflectionHelper.GetMethod(method)!)); + } + + /// + /// Add a middleware method call to this chain's middleware pipeline + /// + /// The chain to add middleware to + /// The middleware class type + /// The name of the method to call + public static void AddMiddleware(this IChain chain, Type middlewareType, string methodName) + { + chain.Middleware.Add(new MethodCall(middlewareType, methodName)); + } + + /// + /// Add a postprocessor method call to this chain's postprocessor pipeline + /// + /// The chain to add the postprocessor to + /// Expression pointing to the postprocessor method + /// The middleware class type + public static void AddPostprocessor(this IChain chain, Expression> method) + { + chain.Postprocessors.Add(new MethodCall(typeof(T), ReflectionHelper.GetMethod(method)!)); + } + + /// + /// Add a postprocessor method call to this chain's postprocessor pipeline + /// + /// The chain to add the postprocessor to + /// The middleware class type + /// The name of the method to call + public static void AddPostprocessor(this IChain chain, Type middlewareType, string methodName) + { + chain.Postprocessors.Add(new MethodCall(middlewareType, methodName)); + } +} + #region sample_IChain ///