FunctionalDdd.Asp 3.0.0-alpha.55

This is a prerelease version of FunctionalDdd.Asp.
There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FunctionalDdd.Asp" Version="3.0.0-alpha.55" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FunctionalDdd.Asp" Version="3.0.0-alpha.55" />
                    
Directory.Packages.props
<PackageReference Include="FunctionalDdd.Asp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FunctionalDdd.Asp --version 3.0.0-alpha.55
                    
#r "nuget: FunctionalDdd.Asp, 3.0.0-alpha.55"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FunctionalDdd.Asp@3.0.0-alpha.55
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FunctionalDdd.Asp&version=3.0.0-alpha.55&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FunctionalDdd.Asp&version=3.0.0-alpha.55&prerelease
                    
Install as a Cake Tool

FunctionalDDD.Asp - ASP.NET Core Extensions

NuGet Package

Comprehensive ASP.NET Core integration for functional domain-driven design, providing:

  1. Automatic Scalar Value Validation - Property-aware error messages with comprehensive error collection
  2. Result-to-HTTP Conversion - Seamless Result<T> to HTTP response mapping
  3. Model Binding - Automatic binding from route/query/form/headers
  4. 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 of IScalarValue.

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:

  1. Automatic validation - DTO properties validated on deserialization
  2. 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

  1. Always use fieldName parameter - Enables property-aware errors
  2. Call validation setup in Program.cs - Required for automatic validation
  3. Add UseScalarValueValidation() middleware - Creates validation scope
  4. Use [ApiController] in MVC - Enables automatic validation responses

Result Conversion

  1. Always pass this to ToActionResult - Required for HTTP context
  2. Use async variants for async operations - ToActionResultAsync, ToHttpResultAsync
  3. Always provide CancellationToken - Enables proper cancellation
  4. Use domain-specific errors - Error.NotFound(), not exceptions
  5. Keep domain logic in domain layer - Controllers orchestrate, not implement

General

  1. Combine approaches wisely - Automatic validation for DTOs, manual for business rules
  2. Use source generator for production - Better performance, AOT support
  3. Test validation thoroughly - Unit test value objects, integration test endpoints

Resources

Examples

License

Part of the FunctionalDDD library. See LICENSE for details.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed