feat: media type & size validation for IFormFile uploads (#216)#218
Conversation
…enAPI (#216) Introduces introspectable, File-level FluentValidation rules that both enforce at runtime and surface metadata for OpenAPI generation: - .FileContentType(params string[]) / .MaxFileSize / .MinFileSize / .FileSizeBetween on IRuleBuilder<T, IFormFile> (core package, shared by all backends). - Swashbuckle: emits requestBody multipart/form-data encoding.<part>.contentType (machine-readable, comma-joined) and a human-readable description note for both content types and size limits. File size is never emitted as maxLength. Root cause of #216: rules on nested IFormFile members (x.File.Length / x.File.ContentType) name the rule "File.Length"/"File.ContentType", which never match the flat schema property "File", so they were silently dropped; and Must() is opaque so allowed content types could not be reflected. The new File-level API fixes both. Encoding is read via the same filtered rule traversal the schema pipeline uses, keeping conditional rules consistent. Tests reproduce the issue scenario and lock the documented limitation of the old nested-member rules. Green on net8.0/net9.0 (Microsoft.OpenApi v1) and net10.0 (OPENAPI_V2); full suite 114 passed, no regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Schema rules (NSwagFluentValidationRuleProvider): FileContentType / FileSize append allowed content types and size limits to the file part description. - New FluentValidationOperationProcessor (IOperationProcessor) emits multipart/form-data encoding for IFormFile parts restricted via .FileContentType(...). Registered as a scoped service; the sample wires it via settings.OperationProcessors.Add(...). Adds a NSwag.Generation package reference for the IOperationProcessor / OpenApi request-body model. Known NSwag limitation: OpenApiEncoding.EncodingType serializes as "encodingType" rather than the OpenAPI-spec "contentType" (verified up to NSwag 14.7.x), so the allowed content types are ALSO surfaced in the part description for guaranteed visibility regardless of the encoding key name. New test project MicroElements.NSwag.FluentValidation.Tests covers the operation processor (encoding emitted, urlencoded not polluted, no-rule => no encoding). Green on net8.0/net9.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mp 7.2.0 (#216) - Microsoft.AspNetCore.OpenApi: FileContentType/FileSize rules append allowed content types and size limits to the file property description (encoding remains out of scope for this backend — its operation transformer has no mutable multipart request-body path). - version 7.1.7 -> 7.2.0 (additive feature, minor bump). - CHANGELOG + README: document the new File-level rules, the backend support matrix, and the limitations (file size is description-only; NSwag encodingType). Full solution green: Swashbuckle 114/114/83 (net8/9/10), NSwag 3/3 (net8/9), AspNetCore.OpenApi 16/16 (net9/10). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent (#216) Spike-turned-test proves (via WebApplicationFactory + /openapi/v1.json) that the allowed content types and size limit DO appear in the native Microsoft.AspNetCore.OpenApi document: on net9 inlined on the multipart `file` property, on net10 on the shared `#/components/schemas/IFormFile` component (the file part is emitted as a $ref). Corrects the README matrix wording: the issue scenario (document reflects the FV rules) works on all three backends; only the extra machine-readable encoding.contentType differs (not emitted by this backend). Documents the net10 shared-IFormFile-component caveat. AspNetCore.OpenApi suite: 17/17 on net9.0 and net10.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ehavior change) - FileUploadMetadata: drop stale x-fileSizeBytes mention from IFileSizeValidator doc (the extension is never emitted; only a description note is). - FileUploadDescription.Append: remove redundant null-forgiving operator (the IsNullOrEmpty guard already proves non-null). - Swashbuckle/NSwag encoding emission: replace the value-tuple FirstOrDefault().Meta? pattern (null-check on a value-type field) with a clearer Where/Select/FirstOrDefault that returns a nullable reference directly; drop the unnecessary Properties.Keys snapshot (the loop mutates Encoding, not Properties); collapse the NSwag encoding upsert create-branch to one line. Green: Swashbuckle 114 (net8) / 83 (net10), NSwag 3 (net8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review this PR |
|
Claude finished @avgalex's task in 2m 30s —— View job PR Review: feat: media type & size validation for IFormFile uploads (#216)
SummaryThis is a well-architected, clean PR. The approach of introducing introspectable FindingsMedium —
|
… spacing
- FileSizeValidator: context-aware default error message that surfaces the
violated bound and its limit ("must not exceed {Max} bytes" / "must be at least
{Min} bytes" / "must be between {Min} and {Max} bytes"), instead of the generic
"has an invalid file size".
- FileSizeBetween: throw ArgumentException when maxBytes < minBytes (fail-fast;
prevents silently documenting an impossible range).
- encoding.contentType / NSwag encodingType: join allowed types with ", " to match
the OpenAPI 3.x spec examples.
- Tests: add MinFileSize and FileSizeBetween description coverage (the min-only and
both-bounds branches of FormatSizeNote).
Deferred (noted on the PR): MIME-format validation of FileContentType (risk of
false rejections on vnd.* / wildcards / parameters) and the pre-existing
undisposed IServiceScope in the NSwag sample.
Green: Swashbuckle 116 (net8) / 85 (net10), NSwag 3, AspNetCore.OpenApi 17/17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review! Addressed in
Deferred (happy to do as follow-ups if you'd like):
All suites green after the changes: Swashbuckle 116/116/85 (net8/9/10), NSwag 3/3 (net8/9), AspNetCore.OpenApi 17/17 (net9/10). |
|
Claude finished @avgalex's task in 1m 44s —— View job Review of commit
|
Adds FileSizeBetween_Throws_When_Bounds_Inverted, asserting the ArgumentException fail-fast guard fires when maxBytes < minBytes (PR review follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks! Addressed in
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #216.
Problem
A user uploads a file via
IFormFileand the generated OpenAPI does not reflect their FluentValidation rules. Two defects:encodingwere never emitted.IFormFilemembers are dropped:RuleFor(x => x.File.Length)/RuleFor(x => x.File.ContentType)are namedFile.Length/File.ContentType, which never match the flat schema propertyFile; andMust(...)carries no introspectable metadata.Solution
New introspectable, File-level FluentValidation rules in the shared core (
MicroElements.OpenApi.FluentValidation.FileUpload) that enforce at runtime AND drive OpenAPI generation:Rules:
.FileContentType(params string[]),.MaxFileSize(long),.MinFileSize(long),.FileSizeBetween(long, long).The allowed content types and size limit are appended to the file part
descriptionon all three backends; Swashbuckle and NSwag additionally emit the machine-readable multipartencoding. Content types reach the operation filter/processor through the same filtered rule traversal the schema pipeline uses, so conditional (.When(...)) rules are included/excluded consistently. File size has no standard OpenAPI byte keyword, so it is description-only (enforcement stays server-side).Backend support
descriptionencoding.contentTypeFluentValidationOperationProcessor(serialized asencodingType— known NSwag limitation)The issue scenario (document reflects the FV rules) works on all three backends — verified for the native AspNetCore.OpenApi pipeline by a
WebApplicationFactorytest against/openapi/v1.json(net9 inlines the description on thefileproperty; net10 puts it on the sharedIFormFilecomponent).Notes / limitations
settings.OperationProcessors.Add(serviceProvider.GetService<FluentValidationOperationProcessor>())(wired in the NSwag sample). Adds aNSwag.Generationpackage reference. NSwag serializes the encoding content type as the non-specencodingType(verified up to NSwag 14.7.x), which is why the description is the guaranteed-visible carrier.encoding.contentTypeis out of scope here (its operation transformer does not write the multipart request body); on net10 theIFormFilecomponent is shared, so per-endpoint content-type rules would accumulate on that one component. These can be follow-ups.Compatibility
Purely additive / opt-in — no existing document output changes. Shipped as pre-release 7.1.8-beta.1 for testing (release). Note: a new public API conventionally warrants a minor bump for the eventual stable.
Tests
IFormFileMediaTypeTests— reproduction of the silently-ignored nested rules + encoding/description/back-compat/urlencoded.MicroElements.NSwag.FluentValidation.Tests— operation processor.Issue216SpikeTests— end-to-end document assertion.Full suite green across all TFMs: Swashbuckle 114/114/83 (net8/9/10), NSwag 3/3 (net8/9), AspNetCore.OpenApi 17/17 (net9/10).
🤖 Generated with Claude Code