Skip to main content

How to Validate Several Fields Together with IValidatableObject in a Minimal API

· 4 min read
Jagdish Kumawat
Founder @ Dewiride

Attributes check one value at a time. When a rule involves more than that, such as "no product twice in one order", or "this number can be anything except 0", put it on the request itself with IValidatableObject. AddValidation calls it, and the error comes back in the same format as the others.

Prerequisites​

Step 1: Implement It​

src/Dewiride.Inventory.Api/Features/Orders/OrderContracts.cs (excerpt)
public sealed record PlaceOrderRequest(
[Required, StringLength(200)] string CustomerName,
[Required, EmailAddress, StringLength(254)] string CustomerEmail,
[Required, MinLength(1), MaxLength(50)] IReadOnlyList<PlaceOrderLine> Lines) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// The order_lines table allows a product once per order.
if (Lines is not null && Lines.Select(l => l.ProductId).Distinct().Count() != Lines.Count)
{
yield return new ValidationResult(
"Each product can appear only once in an order.",
[nameof(Lines)]);
}
}
}

The PlaceOrderRequest record in a macOS-style code window, implementing IValidatableObject, with a Validate method that yields a ValidationResult for duplicate product IDs.

  • yield return adds an error, and you can add several.
  • The second argument names the field, which becomes the key in the response.
  • The null check is needed in case a client sends no list.

Step 2: Break the Rule​

Response
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Lines": [
"Each product can appear only once in an order."
]
},
"traceId": "00-a94e0e0740000cf4049369242667b451-2c623c09b41eaa14-00"
}

A 400 response with the key Lines and the message Each product can appear only once in an order.

Step 3: Use It for One Field, Too​

A range can't say "anything except 0", so a one-line rule goes here as well:

src/Dewiride.Inventory.Api/Features/Stock/StockContracts.cs (excerpt)
public sealed record AdjustStockRequest(
[Range(-1_000_000, 1_000_000)] int QuantityChange,
[Required, StringLength(200)] string Reason,
[StringLength(64)] string? Reference) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (QuantityChange == 0)
{
yield return new ValidationResult(
"The field QuantityChange must not be 0.",
[nameof(QuantityChange)]);
}
}
}
Response
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"QuantityChange": [
"The field QuantityChange must not be 0."
]
},
"traceId": "00-a5e22c0f3bacb5c8e84d42e722a4a381-e412768039e2fa39-00"
}

Step 4: Know the Order​

Validate runs only after every attribute has passed. This request has an empty name and a duplicate product:

Request
POST http://localhost:5190/api/v1/orders
Content-Type: application/json

{"customerName":"","customerEmail":"ana@example.com","lines":[{"productId":1,"quantity":1},{"productId":1,"quantity":1}]}
Response
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"CustomerName": [
"The CustomerName field is required."
]
},
"traceId": "00-80f46b2ed7e1ca0ea3755c3a50fd6c9f-bc286bd2e749df9a-00"
}

Only the name is reported. So a client that fixes it can then get a second error, about the duplicate. That also means Validate can trust its attributes, apart from a null list that's [Required], where it's safer to check anyway.

Common Mistakes​

MistakeWhat happensDo this instead
Going to the database in ValidateValidation gets slow, and races anywayKeep it about the request
Leaving out the member nameThe error has an empty keyPass [nameof(Lines)]
Expecting all errors at onceAttributes come firstTell clients to expect two rounds

Troubleshooting​

SymptomLikely causeFix
Validate isn't calledAn attribute failed, or AddValidation() is missingCheck both
A NullReferenceException in ValidateA missing listCheck for null

Conclusion​

Implement IValidatableObject on the request for any rule that attributes can't express. It runs after the attributes pass, and its errors look like all the others.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.