Table of Contents

Class ServiceCollectionExtensions

Namespace
Trellis.Asp
Assembly
Trellis.Asp.dll

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

builder IMvcBuilder

The 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

services IServiceCollection

The 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:

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

services IServiceCollection

The 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

services IServiceCollection

The 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

services IServiceCollection

The service collection.

configure Action<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

services IServiceCollection

The 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>)

public static IServiceCollection AddTrellisAspWithScalarValidation(this IServiceCollection services, Action<TrellisAspOptions> configure)

Parameters

services IServiceCollection

The service collection.

configure Action<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

services IServiceCollection

The 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"] from Activity.Current?.Id, falling back to HttpContext.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 outgoing Allow header (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

app IApplicationBuilder

The 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

builder TBuilder

The endpoint convention builder.

location InputLocation

Where 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

TBuilder

The 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

location is 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

builder RouteHandlerBuilder

The 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.