diff --git a/CHANGELOG.md b/CHANGELOG.md index 62cd994..91dda78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Changes in 7.0.0.1 +- Fixed: FluentValidation rules not applied to `[FromForm]` parameters (Issue #170) + - Added `RequestBody` processing in `FluentValidationOperationFilter` for `multipart/form-data` and `application/x-www-form-urlencoded` content types + # Changes in 6.1.0 - Added support for .NET 8 and .NET 9 to MicroElements.Swashbuckle.FluentValidation.AspNetCore - Dropped support for .NET 6.0 diff --git a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs index 5b43224..aadbf72 100644 --- a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs +++ b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs @@ -80,9 +80,6 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) private void ApplyInternal(OpenApiOperation operation, OperationFilterContext context) { - if (operation.Parameters == null) - return; - if (_validatorRegistry == null) { _logger.LogWarning(0, "ValidatorFactory is not provided. Please register FluentValidation."); @@ -91,7 +88,19 @@ private void ApplyInternal(OpenApiOperation operation, OperationFilterContext co var schemaProvider = new SwashbuckleSchemaProvider(context.SchemaRepository, context.SchemaGenerator, _schemaGenerationOptions.SchemaIdSelector); - foreach (var operationParameter in operation.Parameters) + // Process operation parameters (FromQuery, FromRoute, FromHeader) + if (operation.Parameters != null) + { + ApplyRulesToParameters(operation, context, schemaProvider); + } + + // Process RequestBody for FromForm and FromBody parameters + ApplyRulesToRequestBody(operation, context, schemaProvider); + } + + private void ApplyRulesToParameters(OpenApiOperation operation, OperationFilterContext context, SwashbuckleSchemaProvider schemaProvider) + { + foreach (var operationParameter in operation.Parameters!) { var apiParameterDescription = context.ApiDescription.ParameterDescriptions.FirstOrDefault(description => description.Name.Equals(operationParameter.Name, StringComparison.InvariantCultureIgnoreCase)); @@ -203,5 +212,88 @@ private void ApplyInternal(OpenApiOperation operation, OperationFilterContext co } } } + + private void ApplyRulesToRequestBody(OpenApiOperation operation, OperationFilterContext context, SwashbuckleSchemaProvider schemaProvider) + { +#if OPENAPI_V2 + var requestBody = operation.RequestBody as OpenApiRequestBody; +#else + var requestBody = operation.RequestBody; +#endif + if (requestBody?.Content == null) + return; + + // Content types used by [FromForm] attribute + var formContentTypes = new[] { "multipart/form-data", "application/x-www-form-urlencoded" }; + + foreach (var contentType in requestBody.Content) + { + if (!formContentTypes.Contains(contentType.Key, StringComparer.OrdinalIgnoreCase)) + continue; + +#if OPENAPI_V2 + var rawSchema = contentType.Value.Schema; + var contentSchema = rawSchema as OpenApiSchema; + string? schemaRefId = rawSchema is OpenApiSchemaReference schemaRef ? schemaRef.Reference?.Id : null; +#else + var contentSchema = contentType.Value.Schema; + string? schemaRefId = contentSchema?.Reference?.Id; +#endif + if (contentSchema == null) + continue; + + // Find the parameter type from ApiDescription + var bodyParameter = context.ApiDescription.ParameterDescriptions + .FirstOrDefault(p => p.Source?.Id == "Form" || p.Source?.Id == "Body"); + + Type? parameterType = null; + if (bodyParameter != null) + { + parameterType = bodyParameter.ModelMetadata?.ContainerType ?? bodyParameter.ModelMetadata?.ModelType; + } + + // If we couldn't find it from body parameter, try to find from schema reference + if (parameterType == null && schemaRefId != null) + { + parameterType = context.ApiDescription.ParameterDescriptions + .Select(p => p.ModelMetadata?.ModelType) + .FirstOrDefault(t => t != null && _schemaGenerationOptions.SchemaIdSelector(t) == schemaRefId); + } + + if (parameterType == null) + continue; + + var validator = _validatorRegistry!.GetValidator(parameterType); + if (validator == null) + continue; + + // Resolve the actual schema (dereference if needed) + OpenApiSchema resolvedSchema = contentSchema; + if (schemaRefId != null) + { + resolvedSchema = schemaProvider.GetSchemaForType(parameterType); + } + + if (resolvedSchema.Properties == null || resolvedSchema.Properties.Count == 0) + continue; + + var schemaContext = new SchemaGenerationContext( + schemaRepository: context.SchemaRepository, + schemaGenerator: context.SchemaGenerator, + schema: resolvedSchema, + schemaType: parameterType, + rules: _rules, + schemaGenerationOptions: _schemaGenerationOptions, + schemaProvider: schemaProvider); + + // Apply validation rules to all properties + FluentValidationSchemaBuilder.ApplyRulesToSchema( + schemaType: parameterType, + schemaPropertyNames: schemaContext.Properties, + validator: validator, + logger: _logger, + schemaGenerationContext: schemaContext); + } + } } } diff --git a/version.props b/version.props index 0736683..e352648 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 7.0.0 + 7.0.0.1