Fixed — [FromForm] rules were ignored for MVC controller actions
Reported in #232 by @bux, as a follow-up to the Swashbuckle-only fix in #170.
Affects MicroElements.AspNetCore.OpenApi.FluentValidation — the integration with the native Microsoft.AspNetCore.OpenApi generator. Minimal APIs were never affected.
[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
[HttpPost]
public IActionResult Post([FromForm] CustomerDto dto) => Ok(); // <- rules were silently dropped
}Root cause: MVC ApiExplorer flattens a controller's [FromForm] complex parameter into one ApiParameterDescription per form field. The generator then builds the request body as an inline schema from those descriptions and never asks for the DTO's JsonTypeInfo — so FluentValidationSchemaTransformer, which hooks into schema generation by type, never saw the DTO at all. A minimal-API form body is not flattened: it gets a $ref to a component schema, which is why that path worked.
FluentValidationOperationTransformer now applies the DTO's rules to the inline form schema, for both multipart/form-data and application/x-www-form-urlencoded. The flattening is detected via ModelMetadata.ContainerType, so a minimal-API body — already owned by the schema transformer — is never processed twice.
Before / after, for Name: NotEmpty().MaximumLength(42) and Age: GreaterThanOrEqualTo(7).LessThanOrEqualTo(99):
"application/x-www-form-urlencoded": {
"schema": {
+ "required": ["Name"],
"type": "object",
"properties": {
- "Name": { "type": "string" },
- "Age": { "type": ["integer","string"] }
+ "Name": { "minLength": 1, "maxLength": 42, "type": "string" },
+ "Age": { "minimum": 7, "maximum": 99, "type": ["integer","string"] }
}
}
}Multiple form parameters
An action binding more than one form parameter composes its request body as an allOf of one property bag per parameter. Every bag is now constrained, and each is paired with the parameter that owns its fields — so two DTOs that happen to declare a property with the same name cannot exchange constraints, and a parameter the action binds directly (a loose scalar, array or IFormFile) keeps its own bag free of any DTO's rules.
Nested form fields
ApiExplorer names a flattened nested property with its binding path (Inner.City). Those keys are deliberately skipped: the rule matcher ignores separators, so such a key would otherwise inherit the rules of an unrelated root property named InnerCity. Nested form fields remain unsupported, matching the Swashbuckle form path.
Also in this release
encoding.contentType(#216) is no longer dropped when an action binds more than one form parameter — the file-part lookup now walks theallOfbags.- The native backend's request-body path consults every registered validator (
GetValidators) rather than only the first, so multi-validator mode (IsOneValidatorForType = false) behaves like the schema path.
Known limitations on this path
- A renamed form field —
[FromForm(Name = "...")], or a property renamed by theINameResolver— gets no constraints. Same class as #230 was for parameters. - On .NET 10, a form part rendered as a
$ref(IFormFile, enums) cannot receive property-level constraints;requiredstill lands correctly. - Unrelated and still open: on .NET 10 a second endpoint binding an
IFormFileerases the #216 file-part notes document-wide (#234, with a full analysis of the mechanism).
Compatibility
Patch release — no public API changes. Swashbuckle and NSwag are unaffected.
Full changelog: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/blob/master/CHANGELOG.md