Class ServiceCollectionExtensions
Extension methods for configuring automatic value object validation in ASP.NET Core.
public static class ServiceCollectionExtensions
- Inheritance
-
ServiceCollectionExtensions
- Inherited Members
Methods
AddScalarValueValidation(IMvcBuilder)
Adds automatic validation for scalar value types during model binding and JSON deserialization.
public static IMvcBuilder AddScalarValueValidation(this IMvcBuilder builder)
Parameters
builderIMvcBuilderThe IMvcBuilder.
Returns
- IMvcBuilder
The IMvcBuilder for chaining.
Examples
Registration in Program.cs:
using Trellis;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddScalarValueValidation();
var app = builder.Build();
app.MapControllers();
app.Run();
Usage in controllers with automatic validation:
public record RegisterUserDto
{
public EmailAddress Email { get; init; } = null!;
public FirstName FirstName { get; init; } = null!;
}
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpPost]
public ActionResult<User> Register(RegisterUserDto dto)
{
// If we reach here, dto is fully validated!
// All scalar values passed validation
var user = User.TryCreate(dto.Email, dto.FirstName);
return user.ToHttpResponse().AsActionResult<User>();
}
}
Remarks
This method configures ASP.NET Core to automatically validate scalar values that implement IScalarValue<TSelf, TPrimitive> during:
- Model binding: Values from route, query, form, or headers
- JSON deserialization: Values from request body (with error collection)
Unlike traditional validation that throws on first error, this approach:
- Collects ALL validation errors during JSON deserialization
- Uses property names (not JSON paths) in error messages
- Returns comprehensive 400 Bad Request with all field errors
- Integrates seamlessly with
[ApiController]attribute
AddScalarValueValidation(IServiceCollection)
Adds automatic value object validation for both MVC Controllers and Minimal APIs. This is a convenience method that configures validation for all ASP.NET Core application types.
public static IServiceCollection AddScalarValueValidation(this IServiceCollection services)
Parameters
servicesIServiceCollectionThe service collection.
Returns
- IServiceCollection
The service collection for chaining.
Examples
Simple setup for mixed applications:
var builder = WebApplication.CreateBuilder(args);
// Unified setup - works for both MVC and Minimal APIs
builder.Services.AddControllers();
builder.Services.AddScalarValueValidation();
var app = builder.Build();
app.UseScalarValueValidation();
app.MapControllers();
app.MapPost("/api/users", (RegisterDto dto) => ...).WithScalarValueValidation();
app.Run();
For MVC-only applications, prefer the fluent API:
builder.Services
.AddControllers()
.AddScalarValueValidation(); // ← Better for MVC-only apps
Remarks
This method is equivalent to calling both:
- AddScalarValueValidation(IMvcBuilder) for MVC JSON options
- AddScalarValueValidationForMinimalApi(IServiceCollection) for Minimal API JSON options
Use this method when you have both controllers and Minimal API endpoints in your application, or when you're not sure which style you'll use. For applications using only one approach, prefer the specific methods for clarity.
Note: In addition to JSON options, this method also configures the
MVC binding/validation pipeline (ScalarValueModelBinderProvider,
Trellis.Asp.Validation.MaybeSuppressChildValidationMetadataProvider,
ScalarValueValidationFilter, and SuppressModelStateInvalidFilter),
so MVC controller hosts that call only this method (or the convenience helper
AddTrellisAspWithScalarValidation(IServiceCollection)) still get the full
scalar-value-object validation experience without having to chain
AddControllers().AddScalarValueValidation(). The MVC registrations are no-ops
when controllers are not added, and are idempotent so combining both calls is safe.
Minimal API endpoint filter and middleware are NOT registered by this method.
Minimal API hosts must additionally call app.UseScalarValueValidation() (middleware)
and chain .WithScalarValueValidation() on each endpoint that should surface
validation as RFC 9457 ProblemDetails.
AddScalarValueValidationForMinimalApi(IServiceCollection)
Configures HTTP JSON options to use property-aware value object validation for Minimal APIs.
public static IServiceCollection AddScalarValueValidationForMinimalApi(this IServiceCollection services)
Parameters
servicesIServiceCollectionThe service collection.
Returns
- IServiceCollection
The service collection for chaining.
Examples
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScalarValueValidationForMinimalApi();
var app = builder.Build();
app.UseScalarValueValidation();
app.MapPost("/users", (RegisterUserDto dto) => ...)
.WithScalarValueValidation();
Remarks
This method configures Minimal API JSON serialization to automatically validate value objects that implement IScalarValue<TSelf, TPrimitive> during JSON deserialization.
For Minimal APIs, also use UseScalarValueValidation(IApplicationBuilder) middleware and WithScalarValueValidation(RouteHandlerBuilder) on your route handlers.
AddTrellisAsp(IServiceCollection)
Registers Trellis ASP.NET Core integration with default error-to-HTTP-status-code mappings.
public static IServiceCollection AddTrellisAsp(this IServiceCollection services)
Parameters
servicesIServiceCollectionThe service collection.
Returns
- IServiceCollection
The service collection for chaining.
Examples
// Error mapping only (no scalar-value JSON / model-binding wiring):
builder.Services.AddTrellisAsp();
// Greenfield default — error mapping + scalar-value validation:
builder.Services.AddTrellisAspWithScalarValidation();
Remarks
This method uses the default error-to-status-code mappings. Most teams can use this zero-configuration overload. Call the overload accepting Action<T> to customize specific mappings.
Does NOT register scalar-value validation. If your endpoints accept request bodies
containing Trellis value objects (IScalarValue<TSelf, TPrimitive>,
Maybe<T>) bound from JSON / route / query, you must additionally call
AddScalarValueValidation(IServiceCollection) — or use the convenience
helper AddTrellisAspWithScalarValidation(IServiceCollection) which composes
both registrations. The split exists so the global MvcOptions / JsonOptions
mutation that scalar validation performs is opt-in instead of silent.
AddTrellisAsp(IServiceCollection, Action<TrellisAspOptions>)
Registers Trellis ASP.NET Core integration with custom error-to-HTTP-status-code mappings.
public static IServiceCollection AddTrellisAsp(this IServiceCollection services, Action<TrellisAspOptions> configure)
Parameters
servicesIServiceCollectionThe service collection.
configureAction<TrellisAspOptions>An action to configure error-to-HTTP-status-code mappings.
Returns
- IServiceCollection
The service collection for chaining.
Examples
builder.Services.AddTrellisAsp(options =>
{
options.MapError<DomainError>(StatusCodes.Status400BadRequest);
});
Remarks
Default mappings are applied first. The configure action can override
any mapping using MapError<TError>(int).
Composition: multiple AddTrellisAsp(...) calls compose. Each call's
configure delegate runs (in registration order) against the same
TrellisAspOptions instance materialized by
OptionsFactory<TrellisAspOptions>, so mappings for distinct
TError types from earlier calls survive. Mappings for the same
TError follow last-wins.
Slot ownership: AddTrellisAsp owns the TrellisAspOptions
service descriptor and replaces any existing one with a bridge factory that resolves
from IOptions<TrellisAspOptions>.Value. Hosts must customize via this
configure action; raw services.AddSingleton(new TrellisAspOptions())
is unsupported and will be silently overwritten the next time AddTrellisAsp runs.
Does NOT register scalar-value validation. See the parameterless overload's remarks for the rationale; use AddTrellisAspWithScalarValidation(IServiceCollection, Action<TrellisAspOptions>) when you also want VO model binding / JSON converter wiring.
AddTrellisAspWithScalarValidation(IServiceCollection)
Convenience helper that composes AddTrellisAsp(IServiceCollection) with AddScalarValueValidation(IServiceCollection). Use this on greenfield projects that want the full Trellis ASP.NET Core surface (error mapping + scalar-value model binding + JSON converters) in a single call.
public static IServiceCollection AddTrellisAspWithScalarValidation(this IServiceCollection services)
Parameters
servicesIServiceCollectionThe service collection.
Returns
- IServiceCollection
The service collection for chaining.
Remarks
Equivalent to calling AddTrellisAsp(IServiceCollection) followed by
AddScalarValueValidation(IServiceCollection). Scalar-value validation
mutates global MvcOptions and JsonOptions; System.Text.Json resolves
converters in registration order with first-match-wins semantics, so if your host
must override Trellis's MaybeScalarValueJsonConverterFactory / MaybePrimitiveJsonConverterFactory
/ ValidatingJsonConverterFactory for some types, register your custom converters
(or your TypeInfoResolver) before calling this helper — or drop down
to AddTrellisAsp(IServiceCollection) + explicit
AddScalarValueValidation(IServiceCollection) so the ordering is visible
at the call site. Naming-policy / general JsonSerializerOptions tweaks that
do not collide with Trellis converters can register before or after.
AddTrellisAspWithScalarValidation(IServiceCollection, Action<TrellisAspOptions>)
Convenience helper that composes AddTrellisAsp(IServiceCollection, Action<TrellisAspOptions>) with AddScalarValueValidation(IServiceCollection).
public static IServiceCollection AddTrellisAspWithScalarValidation(this IServiceCollection services, Action<TrellisAspOptions> configure)
Parameters
servicesIServiceCollectionThe service collection.
configureAction<TrellisAspOptions>An action to configure error-to-HTTP-status-code mappings.
Returns
- IServiceCollection
The service collection for chaining.
AddTrellisProblemDetails(IServiceCollection)
Registers the canonical Trellis ProblemDetails enrichment so unhandled exceptions and
ASP.NET status-code short-circuits (404 / 405 / 415 / 5xx) become RFC 9457 ProblemDetails
responses with a trace id, a support-friendly 500 detail message, and a structured
allow array on 405.
public static IServiceCollection AddTrellisProblemDetails(this IServiceCollection services)
Parameters
servicesIServiceCollectionThe service collection.
Returns
- IServiceCollection
The service collection for chaining.
Examples
builder.Services.AddTrellisProblemDetails();
var app = builder.Build();
app.UseTrellisProblemDetails();
Remarks
Pair with UseTrellisProblemDetails(IApplicationBuilder)
in the request pipeline. Without the matching Use* call, ASP.NET routing short-circuits
(404 / 405 / 415) reach the client as bare status responses with no body.
Composition. This method ensures IProblemDetailsService is registered
(idempotent, safe to call alongside or in lieu of services.AddProblemDetails()).
It then registers a IPostConfigureOptions<TOptions> for
ProblemDetailsOptions that wraps any consumer-supplied
CustomizeProblemDetails delegate. Trellis defaults
run first; the consumer's customization runs last, so consumers can override Trellis
behavior (e.g. tweak the trace-id format, replace the 500 detail message) by setting
CustomizeProblemDetails via services.AddProblemDetails(o => o.CustomizeProblemDetails = ...)
either before or after the call to AddTrellisProblemDetails.
Idempotence. Repeated calls are no-ops past the first; Trellis defaults are applied exactly once per pipeline invocation regardless of how many times this method runs.
What gets enriched.
Extensions["traceId"]fromActivity.Current?.Id, falling back toHttpContext.TraceIdentifier.- Detail on 500 responses is replaced with
"An error occurred in our API. Please share the trace id with our support team."to avoid leaking raw exception detail. Extensions["allow"]on 405 responses contains the methods listed in the outgoingAllowheader (per RFC 9110 §15.5.6) as a structured string array.
UseScalarValueValidation(IApplicationBuilder)
Adds middleware that creates a validation error collection scope for each request. This middleware must be registered in the pipeline to enable validation error collection.
public static IApplicationBuilder UseScalarValueValidation(this IApplicationBuilder app)
Parameters
appIApplicationBuilderThe application builder.
Returns
- IApplicationBuilder
The application builder for chaining.
Examples
var app = builder.Build();
app.UseScalarValueValidation(); // ← Add this before routing
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Remarks
This middleware creates a ValidationErrorsContext scope for each request, allowing ValidatingJsonConverter<TValue, TPrimitive> to collect validation errors during JSON deserialization.
Register this middleware early in the pipeline, before any middleware that deserializes JSON request bodies (such as routing or MVC).
WithInputOrigin<TBuilder>(TBuilder, InputLocation)
Declares where validation failures reaching this endpoint without a location of their own
came from, so they project as that location rather than unknown.
public static TBuilder WithInputOrigin<TBuilder>(this TBuilder builder, InputLocation location) where TBuilder : IEndpointConventionBuilder
Parameters
builderTBuilderThe endpoint convention builder.
locationInputLocationWhere this endpoint's otherwise-unlocated violations came from. Must be Body, Query, or Unspecified to opt out of a route group's declaration.
Returns
- TBuilder
The builder for chaining.
Type Parameters
TBuilderThe endpoint convention builder type.
Examples
app.MapPost("/api/accounts/{id}/deposit", DepositAsync)
.WithInputOrigin(InputLocation.Body);
Remarks
A domain producer names the field that failed but cannot know where the value came from — the same aggregate method is reachable from a worker or a test — so it declines to guess. Model binders stamp the parameters they bind; this fills the remaining gap for the violations that came back up from the domain.
The declaration never overwrites: a violation already located in a route, query or header parameter, or already carrying a body pointer, passes through untouched. Applying it to an endpoint that mixes bound sources is therefore safe.
The nearest declaration wins: applied to a route group it covers every endpoint in the group, and an endpoint that disagrees overrides it by declaring its own. The MVC equivalent is InputOriginAttribute on the action or controller, which adds the same metadata, so the two hosting models cannot drift apart on the wire.
Exceptions
- ArgumentOutOfRangeException
locationis Path, Header, or not a defined InputLocation.
WithScalarValueValidation(RouteHandlerBuilder)
Adds the value object validation endpoint filter to the route handler.
public static RouteHandlerBuilder WithScalarValueValidation(this RouteHandlerBuilder builder)
Parameters
builderRouteHandlerBuilderThe route handler builder.
Returns
- RouteHandlerBuilder
The route handler builder for chaining.
Examples
app.MapPost("/users/register", (RegisterUserDto dto) =>
{
// dto is already validated
return Results.Ok(dto);
}).WithScalarValueValidation();
Remarks
This extension adds ScalarValueValidationEndpointFilter to check for validation errors collected during JSON deserialization.
Ensure UseScalarValueValidation(IApplicationBuilder) middleware is registered and AddScalarValueValidationForMinimalApi(IServiceCollection) is called for full functionality.