Skip to main content
Version: 0.2.x (0.2.1)

Use with ASP.NET Core

Minimal APIs​

Nothing to install. A generated value object implements IParsable<T> and ISpanParsable<T>, which is exactly what minimal API parameter binding looks for, and its [JsonConverter] covers request and response bodies:

app.MapGet("/accounts/{iban}", (Iban iban) => /* … */);

A value that fails to parse is answered with a 400 before the handler runs.

MVC controllers​

dotnet add package AdCodicem.ValueObjects.AspNetCore
builder.Services.AddControllers().AddValueObjects();

That registers a model binder for value objects — routes, query strings, headers, forms — and the JSON options for bodies. The binder is closed over each concrete type, so binding costs one TryParse. An application that configures MVC directly can call AddValueObjects() on MvcOptions instead; that one adds the binder only.

Nullable value objects bind as you would expect: [FromQuery] CountryCode? country is null when the parameter is absent, and a 400 when it is present and rejected.

Problem details carrying the rule​

builder.Services.Configure<ApiBehaviorOptions>(options => options.AddValueObjectProblemDetails());

When model binding rejects a value, the automatic 400 response gains an errorCodes member mapping each rejected parameter to the stable code of the rule it violated:

{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": { "country": ["The value is not one of the accepted values."] },
"errorCodes": { "country": "value_object.not_a_known_value" }
}

A client branches on value_object.not_a_known_value, not on English. The member name is available as ValueObjectProblemDetails.ExtensionName.

This covers what the model binder rejects: route values, query strings, headers and forms. A value inside a JSON body is rejected by the serializer, and the 400 carries its message but no code.

Codes for a payload you validate yourself​

For a payload that carries raw text — an inbound message from another system, say — validate it with FluentValidation and put the codes under the same member, so every 400 of the API has the same shape:

var result = validator.Validate(request);

return result.IsValid
? Results.Accepted()
: Results.ValidationProblem(
result.ToDictionary(),
extensions: new Dictionary<string, object?>
{
[ValueObjectProblemDetails.ExtensionName] =
result.Errors.ToDictionary(failure => failure.PropertyName, failure => failure.ErrorCode),
});