FunctionalDdd.Asp
3.0.0-alpha.55
See the version list below for details.
dotnet add package FunctionalDdd.Asp --version 3.0.0-alpha.55
NuGet\Install-Package FunctionalDdd.Asp -Version 3.0.0-alpha.55
<PackageReference Include="FunctionalDdd.Asp" Version="3.0.0-alpha.55" />
<PackageVersion Include="FunctionalDdd.Asp" Version="3.0.0-alpha.55" />
<PackageReference Include="FunctionalDdd.Asp" />
paket add FunctionalDdd.Asp --version 3.0.0-alpha.55
#r "nuget: FunctionalDdd.Asp, 3.0.0-alpha.55"
#:package FunctionalDdd.Asp@3.0.0-alpha.55
#addin nuget:?package=FunctionalDdd.Asp&version=3.0.0-alpha.55&prerelease
#tool nuget:?package=FunctionalDdd.Asp&version=3.0.0-alpha.55&prerelease
FunctionalDDD.Asp - ASP.NET Core Extensions
Comprehensive ASP.NET Core integration for functional domain-driven design, providing:
- Automatic Scalar Value Validation - Property-aware error messages with comprehensive error collection
- Result-to-HTTP Conversion - Seamless
Result<T>to HTTP response mapping - Model Binding - Automatic binding from route/query/form/headers
- Native AOT Support - Optional source generator for zero-reflection overhead
Table of Contents
Installation
dotnet add package FunctionalDDD.Asp
Scalar Value Validation
Automatically validate types implementing IScalarValue<TSelf, TPrimitive> during JSON deserialization and model binding with property-aware error messages.
Note: This includes DDD value objects (like
ScalarValueObject<T>) as well as any custom implementations ofIScalarValue.
Quick Start
1. Define Value Objects
Value objects that implement IScalarValue get automatic validation:
public class EmailAddress : ScalarValueObject<EmailAddress, string>,
IScalarValue<EmailAddress, string>
{
private EmailAddress(string value) : base(value) { }
public static Result<EmailAddress> TryCreate(string? value, string? fieldName = null)
{
var field = fieldName ?? "email";
if (string.IsNullOrWhiteSpace(value))
return Error.Validation("Email is required.", field);
if (!value.Contains('@'))
return Error.Validation("Email must contain @.", field);
return new EmailAddress(value);
}
}
2. Use in DTOs
public record RegisterUserDto
{
public EmailAddress Email { get; init; } = null!;
public FirstName FirstName { get; init; } = null!;
public string Password { get; init; } = null!;
}
3. Setup Validation
var builder = WebApplication.CreateBuilder(args);
// For MVC Controllers
builder.Services
.AddControllers()
.AddScalarValueValidation();
// For Minimal APIs
builder.Services.AddScalarValueValidationForMinimalApi();
var app = builder.Build();
app.UseScalarValueValidation(); // Required middleware
app.Run();
4. Automatic Validation
[HttpPost]
public IActionResult Register(RegisterUserDto dto)
{
// If we reach here, dto is fully validated!
return Ok(User.Create(dto.Email, dto.FirstName, dto.Password));
}
Request:
{
"email": "invalid",
"firstName": "",
"password": "test"
}
Response (400 Bad Request):
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Email": ["Email must contain @."],
"FirstName": ["Name cannot be empty."]
}
}
MVC Controllers
Full integration with MVC model binding and validation:
builder.Services
.AddControllers()
.AddScalarValueValidation(); // Adds JSON validation + model binding
var app = builder.Build();
app.UseScalarValueValidation(); // Middleware
app.MapControllers();
app.Run();
Features:
- ✅ JSON deserialization with validation
- ✅ Model binding from route/query/form/headers
- ✅ Automatic 400 responses via
ScalarValueValidationFilter - ✅ Integrates with
[ApiController]attribute
Minimal APIs
Endpoint-specific validation with filters:
builder.Services.AddScalarValueValidationForMinimalApi();
var app = builder.Build();
app.UseScalarValueValidation();
app.MapPost("/users", (RegisterUserDto dto) => ...)
.WithScalarValueValidation(); // Add filter to each endpoint
app.Run();
Features:
- ✅ JSON deserialization with validation
- ✅ Endpoint filter for automatic 400 responses
- ⚠️ No automatic model binding (use JSON body)
Model Binding
Value objects automatically bind from various sources in MVC:
// Route parameters
[HttpGet("{userId}")]
public IActionResult GetUser(UserId userId) => Ok(user);
// Query parameters
[HttpGet]
public IActionResult Search([FromQuery] EmailAddress email) => Ok(results);
// Form data
[HttpPost]
public IActionResult Login([FromForm] EmailAddress email, [FromForm] string password) => Ok();
// Headers
[HttpGet]
public IActionResult GetProfile([FromHeader(Name = "X-User-Id")] UserId userId) => Ok();
Native AOT Support
For Native AOT applications, add the source generator:
1. Add Generator Reference
<ItemGroup>
<ProjectReference Include="path/to/AspSourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
2. Mark Your JsonSerializerContext
[GenerateScalarValueConverters] // ← Add this
[JsonSerializable(typeof(RegisterUserDto))]
[JsonSerializable(typeof(User))]
public partial class AppJsonSerializerContext : JsonSerializerContext { }
3. Configure
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default));
The generator automatically:
- Detects all types implementing
IScalarValue - Generates AOT-compatible converters
- Adds
[JsonSerializable]attributes - Enables Native AOT with
<PublishAot>true</PublishAot>
Note: The source generator is optional. Without it, the library uses reflection (works for standard .NET). See docs/REFLECTION-FALLBACK.md for details.
Result Conversion
Convert Railway Oriented Programming Result<T> types to HTTP responses.
Result Conversion: MVC
Use ToActionResult to convert Result<T> to ActionResult<T>:
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpPost]
public ActionResult<User> Register([FromBody] RegisterRequest request) =>
FirstName.TryCreate(request.FirstName)
.Combine(LastName.TryCreate(request.LastName))
.Combine(EmailAddress.TryCreate(request.Email))
.Bind((firstName, lastName, email) =>
User.TryCreate(firstName, lastName, email, request.Password))
.ToActionResult(this);
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUserAsync(
string id,
CancellationToken cancellationToken) =>
await _userRepository.GetByIdAsync(id, cancellationToken)
.ToResultAsync(Error.NotFound($"User {id} not found"))
.ToActionResultAsync(this);
}
Result Conversion: Minimal API
Use ToHttpResult to convert Result<T> to IResult:
var userApi = app.MapGroup("/api/users");
userApi.MapPost("/register", (RegisterUserRequest request) =>
FirstName.TryCreate(request.FirstName)
.Combine(LastName.TryCreate(request.LastName))
.Combine(EmailAddress.TryCreate(request.Email))
.Bind((firstName, lastName, email) =>
User.TryCreate(firstName, lastName, email, request.Password))
.ToHttpResult());
userApi.MapGet("/{id}", async (
string id,
UserRepository repository,
CancellationToken cancellationToken) =>
await repository.GetByIdAsync(id, cancellationToken)
.ToResultAsync(Error.NotFound($"User {id} not found"))
.ToHttpResultAsync());
HTTP Status Mapping
| Result Type | HTTP Status | Description |
|---|---|---|
| Success | 200 OK | Success with content |
| Success (Unit) | 204 No Content | Success without content |
| ValidationError | 400 Bad Request | Validation errors with details |
| BadRequestError | 400 Bad Request | Invalid request |
| UnauthorizedError | 401 Unauthorized | Authentication required |
| ForbiddenError | 403 Forbidden | Access denied |
| NotFoundError | 404 Not Found | Resource not found |
| ConflictError | 409 Conflict | Resource conflict |
| DomainError | 422 Unprocessable Entity | Domain rule violation |
| RateLimitError | 429 Too Many Requests | Rate limit exceeded |
| UnexpectedError | 500 Internal Server Error | Unexpected error |
| ServiceUnavailableError | 503 Service Unavailable | Service unavailable |
Advanced Topics
Property-Aware Error Messages
When the same value object type is used for multiple properties:
public record PersonDto
{
public Name FirstName { get; init; } // ← Same type
public Name LastName { get; init; } // ← Same type
}
Errors correctly show property names:
{
"errors": {
"FirstName": ["Name cannot be empty."],
"LastName": ["Name cannot be empty."]
}
}
Not type names! This requires the fieldName parameter in TryCreate:
public static Result<Name> TryCreate(string? value, string? fieldName = null)
{
var field = fieldName ?? "name"; // ← Use fieldName
if (string.IsNullOrWhiteSpace(value))
return Error.Validation("Name cannot be empty.", field);
return new Name(value);
}
Combining Validation Approaches
You can use both automatic validation and manual Result chaining:
[HttpPost]
public ActionResult<User> Register(RegisterUserDto dto)
{
// dto.Email and dto.FirstName are already validated!
// Now validate business rules:
return UserService.CheckEmailNotTaken(dto.Email)
.Bind(() => User.TryCreate(dto.Email, dto.FirstName, dto.Password))
.ToActionResult(this);
}
This combines:
- Automatic validation - DTO properties validated on deserialization
- Manual validation - Business rules in domain layer
Custom Validation Responses
For Minimal APIs, customize the response:
app.MapPost("/users", (RegisterUserDto dto, HttpContext httpContext) =>
{
var validationError = ValidationErrorsContext.GetValidationError();
if (validationError is not null)
{
return Results.Json(
new { success = false, errors = validationError.ToDictionary() },
statusCode: 422); // Custom status
}
var user = userService.Create(dto);
return Results.Ok(user);
});
Reflection vs Source Generator
| Feature | Reflection | Source Generator |
|---|---|---|
| Setup | Simple (no generator) | Requires analyzer reference |
| Performance | ~50μs overhead at startup | Zero overhead |
| AOT Support | ❌ No | ✅ Yes |
| Trimming | ⚠️ May break | ✅ Safe |
| Use Case | Prototyping, standard .NET | Production, Native AOT |
See docs/REFLECTION-FALLBACK.md for comprehensive comparison.
Best Practices
Scalar Value Validation
- Always use
fieldNameparameter - Enables property-aware errors - Call validation setup in
Program.cs- Required for automatic validation - Add
UseScalarValueValidation()middleware - Creates validation scope - Use
[ApiController]in MVC - Enables automatic validation responses
Result Conversion
- Always pass
thistoToActionResult- Required for HTTP context - Use async variants for async operations -
ToActionResultAsync,ToHttpResultAsync - Always provide CancellationToken - Enables proper cancellation
- Use domain-specific errors -
Error.NotFound(), not exceptions - Keep domain logic in domain layer - Controllers orchestrate, not implement
General
- Combine approaches wisely - Automatic validation for DTOs, manual for business rules
- Use source generator for production - Better performance, AOT support
- Test validation thoroughly - Unit test value objects, integration test endpoints
Resources
- docs/REFLECTION-FALLBACK.md - AOT vs reflection comparison
- generator/README.md - Source generator details
- SAMPLES.md - Comprehensive examples and patterns
- Railway Oriented Programming - Core
Result<T>concepts - Domain-Driven Design - Entity and value object patterns
- PrimitiveValueObjects - Base value object types
Examples
- SampleMinimalApi - Minimal API with Native AOT and source generator
- SampleMinimalApiNoAot - Minimal API with reflection fallback (no source generator) ← Start here!
- SampleWebApplication - MVC controllers with validation
- SampleUserLibrary - Shared value objects
License
Part of the FunctionalDDD library. See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FunctionalDdd.RailwayOrientedProgramming (>= 3.0.0-alpha.55)
- Microsoft.Extensions.DependencyModel (>= 10.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-alpha.72 | 52 | 2/8/2026 |
| 3.0.0-alpha.60 | 52 | 2/4/2026 |
| 3.0.0-alpha.59 | 51 | 2/1/2026 |
| 3.0.0-alpha.56 | 52 | 1/31/2026 |
| 3.0.0-alpha.55 | 51 | 1/31/2026 |
| 3.0.0-alpha.44 | 60 | 1/13/2026 |
| 3.0.0-alpha.20 | 60 | 1/6/2026 |
| 3.0.0-alpha.19 | 64 | 1/5/2026 |
| 3.0.0-alpha.13 | 61 | 1/5/2026 |
| 3.0.0-alpha.9 | 56 | 1/5/2026 |
| 3.0.0-alpha.3 | 164 | 12/20/2025 |
| 2.1.10 | 716 | 12/3/2025 |
| 2.1.9 | 300 | 11/21/2025 |
| 2.1.1 | 260 | 4/26/2025 |
| 2.1.0-preview.3 | 149 | 4/26/2025 |
| 2.0.1 | 256 | 1/23/2025 |
| 2.0.0-alpha.62 | 98 | 1/8/2025 |
| 2.0.0-alpha.61 | 110 | 1/7/2025 |
| 2.0.0-alpha.60 | 124 | 12/7/2024 |
| 2.0.0-alpha.55 | 110 | 11/22/2024 |