Trellis.Asp
3.0.0-alpha.99
dotnet add package Trellis.Asp --version 3.0.0-alpha.99
NuGet\Install-Package Trellis.Asp -Version 3.0.0-alpha.99
<PackageReference Include="Trellis.Asp" Version="3.0.0-alpha.99" />
<PackageVersion Include="Trellis.Asp" Version="3.0.0-alpha.99" />
<PackageReference Include="Trellis.Asp" />
paket add Trellis.Asp --version 3.0.0-alpha.99
#r "nuget: Trellis.Asp, 3.0.0-alpha.99"
#:package Trellis.Asp@3.0.0-alpha.99
#addin nuget:?package=Trellis.Asp&version=3.0.0-alpha.99&prerelease
#tool nuget:?package=Trellis.Asp&version=3.0.0-alpha.99&prerelease
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
- Optional Value Objects —
Maybe<T>support for JSON, model binding, and MVC validation - Native AOT Support — Optional source generator for zero-reflection overhead
Installation
dotnet add package Trellis.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<TSelf, T>) as well as any custom implementations ofIScalarValue.
Quick Start
1. Define Value Objects
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
builder.Services
.AddControllers()
.AddScalarValueValidation(); // Adds JSON validation + model binding
var app = builder.Build();
app.UseScalarValueValidation(); // Middleware
app.MapControllers();
app.Run();
- JSON deserialization with validation
- Model binding from route/query/form/headers
- Automatic 400 responses via
ScalarValueValidationFilter - Integrates with
[ApiController]attribute
Minimal APIs
builder.Services.AddScalarValueValidationForMinimalApi();
var app = builder.Build();
app.UseScalarValueValidation();
app.MapPost("/users", (RegisterUserDto dto) => ...)
.WithScalarValueValidation(); // Add filter to each endpoint
app.Run();
- JSON deserialization with validation
- Endpoint filter for automatic 400 responses
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);
// Headers
[HttpGet]
public IActionResult GetProfile([FromHeader(Name = "X-User-Id")] UserId userId) => Ok();
Optional Value Objects with Maybe<T>
Use Maybe<T> for optional value object properties in DTOs. No additional setup is needed — AddScalarValueValidation() automatically registers the JSON converter, model binder, and validation suppression for Maybe<T> properties.
| JSON Value | Result |
|---|---|
null or absent |
Maybe.None (no error) |
| Valid value | Maybe.From(validated) |
| Invalid value | Validation error collected |
public record RegisterUserDto
{
public FirstName FirstName { get; init; } = null!; // Required
public EmailAddress Email { get; init; } = null!; // Required
public Maybe<Url> Website { get; init; } // Optional
}
Native AOT Support
For Native AOT applications, add the source generator package:
dotnet add package Trellis.AspSourceGenerator
[GenerateScalarValueConverters] // ← Add this
[JsonSerializable(typeof(RegisterUserDto))]
public partial class AppJsonSerializerContext : JsonSerializerContext { }
The generator automatically detects all IScalarValue types, generates AOT-compatible converters, and adds [JsonSerializable] attributes.
Note: The source generator is optional. Without it, the library uses reflection (works for standard .NET).
Result Conversion
Convert Railway Oriented Programming Result<T> types to HTTP responses.
MVC Controllers
[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))
.ToCreatedAtActionResult(this,
actionName: nameof(GetUser),
routeValues: user => new { id = user.Id });
Minimal APIs
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))
.ToCreatedAtRouteHttpResult(
routeName: "GetUser",
routeValues: user => new RouteValueDictionary(new { id = user.Id })));
HTTP Status Mapping
| Result Type | HTTP Status | Description |
|---|---|---|
| Success | 200 OK | Success with content |
| Success (Created) | 201 Created | Resource created with Location header |
| Success (Unit) | 204 No Content | Success without content |
| ValidationError | 400 Bad Request | Validation errors with details |
| BadRequestError | 400 Bad Request | General bad 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 |
Custom Error Mappings
Customize how domain errors map to HTTP status codes:
builder.Services.AddTrellisAsp(options =>
{
options.MapError<DomainError>(StatusCodes.Status400BadRequest);
});
Default mappings are applied automatically. AddTrellisAsp is optional — only needed to override specific mappings.
Property-Aware Error Messages
When the same value object type is used for multiple properties, errors correctly show property names (not type names):
{
"errors": {
"FirstName": ["Name cannot be empty."],
"LastName": ["Name cannot be empty."]
}
}
This requires the fieldName parameter in TryCreate:
public static Result<Name> TryCreate(string? value, string? fieldName = null)
{
var field = fieldName ?? "name";
if (string.IsNullOrWhiteSpace(value))
return Error.Validation("Name cannot be empty.", field);
return new Name(value);
}
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 |
Best Practices
- 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 - Use async variants for async operations —
ToActionResultAsync,ToHttpResultAsync - Combine approaches — Automatic validation for DTOs, manual
Resultchaining for business rules
Related Packages
- Trellis.Results — Core
Result<T>type - Trellis.Primitives — Base value object types
- Trellis.DomainDrivenDesign — Entity and aggregate patterns
- Trellis.AspSourceGenerator — AOT source generator
License
MIT — 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
- Microsoft.Extensions.DependencyModel (>= 10.0.2)
- Trellis.Results (>= 3.0.0-alpha.99)
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.99 | 0 | 3/4/2026 |
| 3.0.0-alpha.98 | 25 | 3/3/2026 |
| 3.0.0-alpha.95 | 42 | 3/2/2026 |
| 3.0.0-alpha.94 | 33 | 3/2/2026 |
| 3.0.0-alpha.93 | 42 | 3/1/2026 |
| 3.0.0-alpha.92 | 56 | 2/28/2026 |
| 3.0.0-alpha.83 | 42 | 2/27/2026 |