Skip to content

Commit 7196097

Browse files
committed
docs: Add Integration Packages documentation (ASP.NET Core, EF Core, FluentValidation, OneOf)
1 parent 25eab20 commit 7196097

9 files changed

Lines changed: 1818 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "Integration Packages",
3+
"position": 5,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Framework integrations for ASP.NET Core, Entity Framework Core, FluentValidation, and OneOf compatibility."
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "ASP.NET Core",
3+
"position": 1,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Build type-safe Web APIs with automatic ProblemDetails generation and HTTP response mapping."
7+
}
8+
}
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
---
2+
sidebar_position: 1
3+
---
4+
5+
# ASP.NET Core Integration
6+
7+
The UnionGenerator.AspNetCore package provides seamless integration with ASP.NET Core, enabling you to build type-safe Web APIs with automatic ProblemDetails generation and HTTP response mapping.
8+
9+
## Overview
10+
11+
This integration package bridges the gap between discriminated unions and HTTP responses, making it easy to:
12+
13+
- Convert union types to appropriate HTTP responses
14+
- Generate RFC 7807 ProblemDetails automatically
15+
- Use unions in controller actions and minimal APIs
16+
- Handle validation errors consistently
17+
- Map domain errors to HTTP status codes
18+
19+
## Installation
20+
21+
```bash
22+
dotnet add package UnionGenerator.AspNetCore
23+
```
24+
25+
**Requirements:**
26+
- .NET 6.0 or later
27+
- ASP.NET Core 6.0 or later
28+
- UnionGenerator package
29+
30+
## Quick Start
31+
32+
### Define Your Union
33+
34+
```csharp
35+
using UnionGenerator;
36+
37+
[GenerateUnion]
38+
public partial record UserResult
39+
{
40+
public static partial UserResult Success(User user);
41+
public static partial UserResult NotFound(string userId);
42+
public static partial UserResult ValidationError(List<string> errors);
43+
}
44+
```
45+
46+
### Use in Controller
47+
48+
```csharp
49+
[ApiController]
50+
[Route("api/users")]
51+
public class UsersController : ControllerBase
52+
{
53+
[HttpGet("{id}")]
54+
public IActionResult GetUser(string id)
55+
{
56+
var result = _userService.GetUser(id);
57+
58+
return result.Match(
59+
success => Ok(success.User),
60+
notFound => NotFound(new ProblemDetails
61+
{
62+
Title = "User not found",
63+
Detail = $"User '{notFound.UserId}' does not exist"
64+
}),
65+
validationError => BadRequest(validationError.Errors)
66+
);
67+
}
68+
}
69+
```
70+
71+
### Use in Minimal APIs
72+
73+
```csharp
74+
app.MapGet("/api/users/{id}", (string id, IUserService service) =>
75+
{
76+
var result = service.GetUser(id);
77+
78+
return result.Match(
79+
success => Results.Ok(success.User),
80+
notFound => Results.NotFound(new { Error = $"User {notFound.UserId} not found" }),
81+
validationError => Results.BadRequest(new { Errors = validationError.Errors })
82+
);
83+
});
84+
```
85+
86+
## Core Features
87+
88+
### Result to HTTP Response Mapping
89+
90+
Convert unions directly to HTTP responses:
91+
92+
```csharp
93+
[GenerateUnion]
94+
public partial record ApiResult<T>
95+
{
96+
public static partial ApiResult<T> Ok(T data);
97+
public static partial ApiResult<T> NotFound(string resource);
98+
public static partial ApiResult<T> BadRequest(string message);
99+
public static partial ApiResult<T> Unauthorized();
100+
}
101+
102+
[HttpGet("{id}")]
103+
public IActionResult GetItem(int id)
104+
{
105+
return _service.GetItem(id).Match(
106+
ok => Ok(ok.Data),
107+
notFound => NotFound(new { Error = notFound.Resource }),
108+
badRequest => BadRequest(new { Error = badRequest.Message }),
109+
unauthorized => Unauthorized()
110+
);
111+
}
112+
```
113+
114+
### ProblemDetails Integration
115+
116+
Generate RFC 7807 compliant error responses:
117+
118+
```csharp
119+
public static class ResultExtensions
120+
{
121+
public static IActionResult ToProblemDetails<T>(this ApiResult<T> result)
122+
{
123+
return result.Match(
124+
ok => new OkObjectResult(ok.Data),
125+
notFound => new NotFoundObjectResult(new ProblemDetails
126+
{
127+
Status = 404,
128+
Title = "Resource not found",
129+
Detail = notFound.Resource,
130+
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"
131+
}),
132+
badRequest => new BadRequestObjectResult(new ProblemDetails
133+
{
134+
Status = 400,
135+
Title = "Bad request",
136+
Detail = badRequest.Message
137+
}),
138+
unauthorized => new UnauthorizedObjectResult(new ProblemDetails
139+
{
140+
Status = 401,
141+
Title = "Unauthorized"
142+
})
143+
);
144+
}
145+
}
146+
```
147+
148+
## CRUD Operations
149+
150+
Complete example with all operations:
151+
152+
```csharp
153+
[GenerateUnion]
154+
public partial record UserOperationResult
155+
{
156+
public static partial UserOperationResult Created(User user);
157+
public static partial UserOperationResult Updated(User user);
158+
public static partial UserOperationResult Deleted();
159+
public static partial UserOperationResult NotFound();
160+
public static partial UserOperationResult ValidationError(Dictionary<string, string[]> errors);
161+
public static partial UserOperationResult Conflict(string message);
162+
}
163+
164+
[ApiController]
165+
[Route("api/users")]
166+
public class UsersController : ControllerBase
167+
{
168+
[HttpPost]
169+
public IActionResult Create(CreateUserDto dto)
170+
{
171+
return _service.CreateUser(dto).Match(
172+
created => CreatedAtAction(nameof(Get), new { id = created.User.Id }, created.User),
173+
updated => throw new InvalidOperationException(),
174+
deleted => throw new InvalidOperationException(),
175+
notFound => throw new InvalidOperationException(),
176+
validationError => BadRequest(new ValidationProblemDetails(validationError.Errors)),
177+
conflict => Conflict(new { Error = conflict.Message })
178+
);
179+
}
180+
181+
[HttpGet("{id}")]
182+
public IActionResult Get(int id)
183+
{
184+
return _service.GetUser(id).Match(
185+
created => Ok(created.User),
186+
updated => Ok(updated.User),
187+
deleted => throw new InvalidOperationException(),
188+
notFound => NotFound(),
189+
validationError => throw new InvalidOperationException(),
190+
conflict => throw new InvalidOperationException()
191+
);
192+
}
193+
194+
[HttpPut("{id}")]
195+
public IActionResult Update(int id, UpdateUserDto dto)
196+
{
197+
return _service.UpdateUser(id, dto).Match(
198+
created => throw new InvalidOperationException(),
199+
updated => Ok(updated.User),
200+
deleted => throw new InvalidOperationException(),
201+
notFound => NotFound(),
202+
validationError => BadRequest(new ValidationProblemDetails(validationError.Errors)),
203+
conflict => Conflict(new { Error = conflict.Message })
204+
);
205+
}
206+
207+
[HttpDelete("{id}")]
208+
public IActionResult Delete(int id)
209+
{
210+
return _service.DeleteUser(id).Match(
211+
created => throw new InvalidOperationException(),
212+
updated => throw new InvalidOperationException(),
213+
deleted => NoContent(),
214+
notFound => NotFound(),
215+
validationError => throw new InvalidOperationException(),
216+
conflict => Conflict(new { Error = conflict.Message })
217+
);
218+
}
219+
}
220+
```
221+
222+
## Validation Errors
223+
224+
Handle validation errors with ValidationProblemDetails:
225+
226+
```csharp
227+
[GenerateUnion]
228+
public partial record CreateUserResult
229+
{
230+
public static partial CreateUserResult Success(User user);
231+
public static partial CreateUserResult ValidationFailed(Dictionary<string, string[]> errors);
232+
}
233+
234+
[HttpPost]
235+
public IActionResult CreateUser(CreateUserDto dto)
236+
{
237+
return _service.CreateUser(dto).Match(
238+
success => CreatedAtAction(nameof(GetUser), new { id = success.User.Id }, success.User),
239+
validationFailed => BadRequest(new ValidationProblemDetails(validationFailed.Errors)
240+
{
241+
Title = "Validation failed",
242+
Detail = "One or more validation errors occurred"
243+
})
244+
);
245+
}
246+
```
247+
248+
## File Upload
249+
250+
Handle file uploads with unions:
251+
252+
```csharp
253+
[GenerateUnion]
254+
public partial record FileUploadResult
255+
{
256+
public static partial FileUploadResult Uploaded(string fileId, long size);
257+
public static partial FileUploadResult TooLarge(long maxSize);
258+
public static partial FileUploadResult InvalidFormat(string[] allowedFormats);
259+
}
260+
261+
[HttpPost("upload")]
262+
public IActionResult Upload(IFormFile file)
263+
{
264+
return _service.Upload(file).Match(
265+
uploaded => Ok(new { FileId = uploaded.FileId, Size = uploaded.Size }),
266+
tooLarge => BadRequest(new ProblemDetails
267+
{
268+
Title = "File too large",
269+
Detail = $"Maximum size is {tooLarge.MaxSize} bytes"
270+
}),
271+
invalidFormat => BadRequest(new ProblemDetails
272+
{
273+
Title = "Invalid format",
274+
Detail = $"Allowed: {string.Join(", ", invalidFormat.AllowedFormats)}"
275+
})
276+
);
277+
}
278+
```
279+
280+
## Best Practices
281+
282+
### Consistent Error Types
283+
284+
Define standard error types for your API:
285+
286+
```csharp
287+
[GenerateUnion]
288+
public partial record ApiError
289+
{
290+
public static partial ApiError NotFound(string resource, string id);
291+
public static partial ApiError ValidationFailed(Dictionary<string, string[]> errors);
292+
public static partial ApiError Unauthorized();
293+
public static partial ApiError Forbidden(string reason);
294+
public static partial ApiError Conflict(string message);
295+
public static partial ApiError ServerError(Exception exception);
296+
}
297+
```
298+
299+
### Extension Methods
300+
301+
Create reusable extension methods:
302+
303+
```csharp
304+
public static class ApiErrorExtensions
305+
{
306+
public static IActionResult ToActionResult(this ApiError error)
307+
{
308+
return error.Match(
309+
notFound => CreateNotFound(notFound),
310+
validationFailed => CreateValidationError(validationFailed),
311+
unauthorized => new UnauthorizedResult(),
312+
forbidden => CreateForbidden(forbidden),
313+
conflict => CreateConflict(conflict),
314+
serverError => CreateServerError(serverError)
315+
);
316+
}
317+
318+
private static IActionResult CreateNotFound(ApiError.NotFoundCase notFound) =>
319+
new NotFoundObjectResult(new ProblemDetails
320+
{
321+
Status = 404,
322+
Title = "Not found",
323+
Detail = $"{notFound.Resource} with ID '{notFound.Id}' not found"
324+
});
325+
326+
// ... other methods
327+
}
328+
```
329+
330+
### Middleware Integration
331+
332+
Handle exceptions globally:
333+
334+
```csharp
335+
public class ExceptionHandlingMiddleware
336+
{
337+
private readonly RequestDelegate _next;
338+
339+
public async Task InvokeAsync(HttpContext context)
340+
{
341+
try
342+
{
343+
await _next(context);
344+
}
345+
catch (Exception ex)
346+
{
347+
var error = ApiError.ServerError(ex);
348+
var result = error.ToActionResult();
349+
350+
context.Response.StatusCode = GetStatusCode(result);
351+
await context.Response.WriteAsJsonAsync(GetProblemDetails(result));
352+
}
353+
}
354+
}
355+
```
356+
357+
## Key Takeaways
358+
359+
✅ **Match method** provides natural conversion to HTTP responses
360+
✅ **ProblemDetails** ensures RFC 7807 compliance
361+
✅ **Type safety** eliminates magic status codes
362+
✅ **Consistent patterns** across all endpoints
363+
✅ **Works with both** controllers and minimal APIs
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "Entity Framework Core",
3+
"position": 2,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Store and query union types in databases with automatic value converters."
7+
}
8+
}

0 commit comments

Comments
 (0)