> ## Documentation Index
> Fetch the complete documentation index at: https://ancplua.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Endpoint Generator

> Source generator for ASP.NET Core Minimal API integration

The `ErrorOrX.Generators` package provides a Roslyn source generator that converts `ErrorOr<T>` handlers into ASP.NET Core Minimal API endpoints.

## What Gets Generated

```csharp theme={null}
// You write:
[Get("/todos/{id}")]
public static ErrorOr<Todo> GetById(Guid id) => _db.Find(id) ?? Error.NotFound();

// Generator produces:
app.MapGet("/todos/{id}", Invoke_Ep1)
    .WithName("MyApp_TodoEndpoints_GetById");

private static async Task Invoke_Ep1(HttpContext ctx)
{
    var __result = await Invoke_Ep1_Core(ctx);
    await __result.ExecuteAsync(ctx);
}

private static Task<IResult> Invoke_Ep1_Core(HttpContext ctx)
{
    Guid id = Guid.Parse((string)ctx.Request.RouteValues["id"]!);
    var result = TodoEndpoints.GetById(id);
    if (result.IsError) return Task.FromResult(ToProblem(result.Errors));
    return Task.FromResult(TypedResults.Ok(result.Value));
}
```

## Route Attributes

| Attribute           | HTTP Method |
| ------------------- | ----------- |
| `[Get("/path")]`    | GET         |
| `[Post("/path")]`   | POST        |
| `[Put("/path")]`    | PUT         |
| `[Patch("/path")]`  | PATCH       |
| `[Delete("/path")]` | DELETE      |

## Parameter Binding

The generator infers parameter sources based on HTTP method and type.

### Binding Priority

| Priority | Condition                                                 | Binding          |
| -------- | --------------------------------------------------------- | ---------------- |
| 1        | Explicit attribute (`[FromBody]`, `[FromServices]`, etc.) | As specified     |
| 2        | Special types (`HttpContext`, `CancellationToken`)        | Auto-detected    |
| 3        | Parameter name matches route `{param}`                    | Route            |
| 4        | Primitive type not in route                               | Query            |
| 5        | Interface type                                            | Service          |
| 6        | Abstract type                                             | Service          |
| 7        | Service naming pattern                                    | Service          |
| 8        | POST/PUT/PATCH + complex type                             | **Body**         |
| 9        | GET/DELETE + complex type                                 | **Error EOE025** |
| 10       | Fallback                                                  | Service          |

### Service Type Detection

These patterns are detected as service types:

```csharp theme={null}
ITodoService      // Interface with Service suffix
IUserRepository   // Interface with Repository suffix
TodoHandler       // *Handler
TodoManager       // *Manager
ConfigProvider    // *Provider
TodoFactory       // *Factory
HttpClient        // *Client
AppDbContext      // *Context with Db
```

### Complex Type on GET/DELETE

Complex types on GET/DELETE require explicit binding:

```csharp theme={null}
// ⚠️ EOE025: Ambiguous parameter binding
[Get("/todos")]
public static ErrorOr<List<Todo>> Search(SearchFilter filter) => ...

// ✅ Explicit query binding
[Get("/todos")]
public static ErrorOr<List<Todo>> Search([FromQuery] SearchFilter filter) => ...

// ✅ Or use AsParameters
[Get("/todos")]
public static ErrorOr<List<Todo>> Search([AsParameters] SearchFilter filter) => ...
```

## Interface Types with `[ReturnsError]`

<Warning>
  **Without `[ReturnsError]`, your OpenAPI spec is incomplete.**

  When an endpoint delegates to a service method, the generator can only see the return type `ErrorOr<T>` - it has no idea what errors that method might return. Your Swagger UI shows only `200 OK` and generic `500`, while your API actually returns `404`, `400`, `403`... Generated API clients miss error handling for real responses.
</Warning>

### The Problem

```csharp theme={null}
public interface ITodoService
{
    // Generator sees: ErrorOr<Todo>
    // Generator doesn't know: this can return NotFound, Validation errors
    ErrorOr<Todo> GetByIdAsync(Guid id, CancellationToken ct);
}

[Get("/todos/{id}")]
public static Task<ErrorOr<Todo>> GetById(Guid id, ITodoService svc, CancellationToken ct) =>
    svc.GetByIdAsync(id, ct);

// ❌ Generated OpenAPI: only 200 OK, 500 Internal Server Error
// ❌ Swagger UI: misleading - doesn't show 404 is possible
// ❌ Generated clients: no 404 handling code
```

### The Solution

Declare possible errors on the interface contract:

```csharp theme={null}
public interface ITodoService
{
    [ReturnsError(ErrorType.NotFound, "Todo.NotFound")]
    ErrorOr<Todo> GetByIdAsync(Guid id, CancellationToken ct);
}

// ✅ Generated OpenAPI: 200 OK, 404 Not Found, 500 Internal Server Error
// ✅ Swagger UI: shows all response codes with ProblemDetails schema
// ✅ Generated clients: proper error handling for each status code
```

The generator reads `[ReturnsError]` and includes the corresponding `TypedResult` in the `Results<...>` union:

```csharp theme={null}
// Generated
Results<Ok<Todo>, NotFound<ProblemDetails>>
```

### Multiple Error Types

Real-world operations have multiple failure modes. Declare them all:

```csharp theme={null}
public interface IOrderService
{
    [ReturnsError(ErrorType.NotFound, "Order.NotFound")]
    [ReturnsError(ErrorType.Forbidden, "Order.AccessDenied")]
    [ReturnsError(ErrorType.Conflict, "Order.AlreadyShipped")]
    ErrorOr<Order> ShipAsync(Guid orderId, CancellationToken ct);
}

[Post("/orders/{orderId}/ship")]
public static Task<ErrorOr<Order>> Ship(Guid orderId, IOrderService svc, CancellationToken ct) =>
    svc.ShipAsync(orderId, ct);

// Generated: Results<Ok<Order>, NotFound<ProblemDetails>, ForbiddenHttpResult, Conflict<ProblemDetails>>
```

Now your OpenAPI spec is **accurate**: clients know shipping can fail with 404 (order doesn't exist), 403 (not authorized), or 409 (already shipped).

## Middleware Attributes

The generator emits middleware fluent calls since the wrapper delegate loses original method attributes.

<Warning>
  This is security-critical. ASP.NET Core only sees attributes on the delegate
  passed to `MapGet()`/`MapPost()`. Since ErrorOrX generates a wrapper method,
  the original method's attributes are invisible to ASP.NET. The generator MUST
  emit equivalent fluent calls.
</Warning>

### Authorization

| Attribute                               | Generated Call                      |
| --------------------------------------- | ----------------------------------- |
| `[Authorize]`                           | `.RequireAuthorization()`           |
| `[Authorize("Policy")]`                 | `.RequireAuthorization("Policy")`   |
| `[Authorize("P1")]` `[Authorize("P2")]` | `.RequireAuthorization("P1", "P2")` |
| `[AllowAnonymous]`                      | `.AllowAnonymous()`                 |

Multiple `[Authorize]` attributes with different policies are accumulated and emitted as a single call:

```csharp theme={null}
[Get("/admin/secrets")]
[Authorize("AdminPolicy")]
[Authorize("AuditPolicy")]
public static ErrorOr<string> GetSecrets() => "top-secret";

// Generated: .RequireAuthorization("AdminPolicy", "AuditPolicy")
```

<Note>
  `[AllowAnonymous]` overrides `[Authorize]`. When both are present, only
  `.AllowAnonymous()` is emitted.
</Note>

### Rate Limiting

| Attribute                        | Generated Call                   |
| -------------------------------- | -------------------------------- |
| `[EnableRateLimiting("policy")]` | `.RequireRateLimiting("policy")` |
| `[DisableRateLimiting]`          | `.DisableRateLimiting()`         |

<Note>
  `[DisableRateLimiting]` overrides `[EnableRateLimiting]`. When both are
  present, only `.DisableRateLimiting()` is emitted.
</Note>

### Output Caching

| Attribute                         | Generated Call                                          |
| --------------------------------- | ------------------------------------------------------- |
| `[OutputCache]`                   | `.CacheOutput()`                                        |
| `[OutputCache(PolicyName = "x")]` | `.CacheOutput("x")`                                     |
| `[OutputCache(Duration = 60)]`    | `.CacheOutput(p => p.Expire(TimeSpan.FromSeconds(60)))` |

### CORS

| Attribute                | Generated Call           |
| ------------------------ | ------------------------ |
| `[EnableCors]`           | `.RequireCors()`         |
| `[EnableCors("Policy")]` | `.RequireCors("Policy")` |
| `[DisableCors]`          | (disables CORS)          |

### Combining Multiple Middleware

All middleware attributes can be combined on a single endpoint:

```csharp theme={null}
[Get("/api/data")]
[Authorize("ApiPolicy")]
[EnableRateLimiting("standard")]
[OutputCache(PolicyName = "ApiCache")]
[EnableCors("Production")]
public static ErrorOr<Data> GetData() => ...

// Generated:
// app.MapGet("/api/data", Invoke_Ep0)
//     .RequireAuthorization("ApiPolicy")
//     .RequireRateLimiting("standard")
//     .CacheOutput("ApiCache")
//     .RequireCors("Production")
//     .WithName("Api_GetData")
//     .WithTags("Api");
```

## JSON Context Generation

<Warning>
  Roslyn source generators cannot see output from other generators. If ErrorOrX
  generates a `JsonSerializerContext`, the System.Text.Json source generator
  will NOT process it, causing runtime errors in Native AOT. **You MUST create
  your own `JsonSerializerContext`.**
</Warning>

### Default Behavior

When `ErrorOrGenerateJsonContext` is `false` (default):

```csharp theme={null}
// Generated ErrorOrJsonContext.g.cs
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Todo))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(HttpValidationProblemDetails))]
internal partial class ErrorOrJsonContext : JsonSerializerContext { }
```

### With Custom Context

Disable generation and use your own:

```xml theme={null}
<PropertyGroup>
  <ErrorOrGenerateJsonContext>false</ErrorOrGenerateJsonContext>
</PropertyGroup>
```

The generator emits a helper file with copy-paste attributes:

```csharp theme={null}
// ErrorOrJsonContext.MissingTypes.g.cs
// Add these to your JsonSerializerContext:
// [JsonSerializable(typeof(Microsoft.AspNetCore.Mvc.ProblemDetails))]
// [JsonSerializable(typeof(Microsoft.AspNetCore.Http.HttpValidationProblemDetails))]
```

## MSBuild Properties

| Property                     | Default | Purpose                             |
| ---------------------------- | ------- | ----------------------------------- |
| `ErrorOrGenerateJsonContext` | `false` | Generate JSON serialization context |

<Note>
  `ErrorOrGenerateJsonContext` is disabled by default because generated JSON
  contexts cannot be processed by System.Text.Json's source generator. See the
  warning above.
</Note>

## AOT Compatibility

The generator produces AOT-compatible code:

1. **No `(Delegate)` cast** - Uses typed `MapGet`/`MapPost`
2. **Wrapper pattern** - Returns `Task`, not `Task<Results<...>>`
3. **Explicit `ExecuteAsync`** - Handles response serialization

```csharp theme={null}
// Wrapper: matches RequestDelegate (HttpContext → Task)
private static async Task Invoke_Ep1(HttpContext ctx)
{
    var __result = await Invoke_Ep1_Core(ctx);
    await __result.ExecuteAsync(ctx);  // Writes response
}

// Core: typed for OpenAPI documentation
private static Task<IResult> Invoke_Ep1_Core(HttpContext ctx)
{
    // Handler logic
}
```

## Service Registration

The builder pattern follows ASP.NET Core conventions (like `AddRazorComponents()`):

```csharp theme={null}
// Program.cs
builder.Services.AddErrorOrEndpoints()
    .UseJsonContext<AppJsonSerializerContext>()
    .WithCamelCase()
    .WithIgnoreNulls();
```

### Available Methods

| Method                       | Purpose                                     |
| ---------------------------- | ------------------------------------------- |
| `UseJsonContext<TContext>()` | Register AOT-compatible JSON context        |
| `WithCamelCase(bool)`        | Use camelCase for JSON properties (default) |
| `WithIgnoreNulls(bool)`      | Ignore null values in JSON (default)        |

<Note>
  Calling `MapErrorOrEndpoints()` without `AddErrorOrEndpoints()` throws an
  `InvalidOperationException` with a clear error message.
</Note>

## Endpoint Mapping

`MapErrorOrEndpoints()` returns an `IEndpointConventionBuilder` for global configuration:

```csharp theme={null}
// Apply conventions to ALL ErrorOr endpoints
app.MapErrorOrEndpoints()
    .RequireAuthorization()           // All endpoints require auth
    .RequireRateLimiting("api")       // Rate limiting for all
    .RequireCors("Production")        // CORS policy
    .WithGroupName("v1");             // OpenAPI grouping
```

This follows ASP.NET Core patterns like `MapRazorComponents()`.

## API Versioning

Full API versioning support with `Asp.Versioning.Http`:

```csharp theme={null}
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public static class TodoEndpoints
{
    [Get("/todos")]
    public static ErrorOr<List<Todo>> GetAll() => ...

    [MapToApiVersion("2.0")]
    [Get("/todos/{id}")]
    public static ErrorOr<Todo> GetByIdV2(Guid id) => ...
}

// Version-neutral
[ApiVersionNeutral]
public static class HealthEndpoints
{
    [Get("/health")]
    public static ErrorOr<HealthStatus> Check() => ...
}
```

<Card title="API Versioning Guide" icon="code-branch" href="/erroror/api-versioning">
  Complete guide to API versioning including version formats, generated code,
  service registration, and diagnostics (EOE050-EOE055).
</Card>
