diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c4bed6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,158 @@ +[*] +charset = utf-8-bom +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 +max_line_length = 960 + +[*.yml] +indent_size = 2 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_space_after_cast = true +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_naming_rule.interfaces_rule.import_to_resharper = True +dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces +dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7 +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper = True +dotnet_naming_rule.private_constants_rule.resharper_description = Constant fields (private) +dotnet_naming_rule.private_constants_rule.resharper_guid = 236f7aa5-7b06-43ca-bf2a-9b31bfcff09a +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_instance_fields_rule.resharper_description = Instance fields (private) +dotnet_naming_rule.private_instance_fields_rule.resharper_guid = 4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper = True +dotnet_naming_rule.private_static_fields_rule.resharper_description = Static fields (private) +dotnet_naming_rule.private_static_fields_rule.resharper_guid = f9fce829-e6f4-4cb2-80f1-5497c44f51df +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper = True +dotnet_naming_rule.private_static_readonly_rule.resharper_description = Static readonly fields (private) +dotnet_naming_rule.private_static_readonly_rule.resharper_guid = 15b5b1f1-457c-4ca6-b278-5615aedc07d3 +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper = True +dotnet_naming_rule.type_parameters_rule.resharper_description = Type parameters +dotnet_naming_rule.type_parameters_rule.resharper_guid = 2c62818f-621b-4425-adc9-78611099bfcb +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix = I +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix = T +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field +dotnet_naming_symbols.private_constants_symbols.required_modifiers = const +dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds = constant_field +dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds = field,readonly_field +dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds = field +dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = readonly,static +dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds = readonly_field +dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_autodetect_indent_settings = true +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_accessor_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false +resharper_space_within_single_line_array_initializer_braces = false +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_mvc_action_not_resolved_highlighting = warning +resharper_mvc_area_not_resolved_highlighting = warning +resharper_mvc_controller_not_resolved_highlighting = warning +resharper_mvc_masterpage_not_resolved_highlighting = warning +resharper_mvc_partial_view_not_resolved_highlighting = warning +resharper_mvc_template_not_resolved_highlighting = warning +resharper_mvc_view_component_not_resolved_highlighting = warning +resharper_mvc_view_component_view_not_resolved_highlighting = warning +resharper_mvc_view_not_resolved_highlighting = warning +resharper_razor_assembly_not_resolved_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_style = space +indent_size = 2 + +[*.scss] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 7594aaf..b762f75 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -4,11 +4,9 @@ env: JAVA_VERSION: 17 JAVA_DISTRIBUTION: microsoft DOTNET_VERSION: | - 3.1.x - 6.0.x - 7.0.x 8.0.x 9.0.x + 10.0.x DOTNET_BUILD_CONFIGURATION: Release SONAR_PATH: .\.sonar\scanner SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} @@ -29,18 +27,18 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} - name: Set up .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index a970688..68abb60 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -1,7 +1,7 @@ name: FluentValidation.AutoValidation [Release] env: - DOTNET_VERSION: 9.0.x + DOTNET_VERSION: 10.0.x DOTNET_BUILD_CONFIGURATION: Release DOTNET_PACKAGES_OUTPUT_DIRECTORY: .nuget NUGET_SOURCE: https://api.nuget.org/v3/index.json @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/Directory.Build.props b/Directory.Build.props index ff8f997..7e6f786 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ + net8.0;net9.0;net10.0 enable - 8.0 NU1701 true true diff --git a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj index 5019ec4..4eb4038 100644 --- a/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj +++ b/FluentValidation.AutoValidation.Endpoints/FluentValidation.AutoValidation.Endpoints.csproj @@ -5,7 +5,6 @@ - net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Endpoints SharpGrip.FluentValidation.AutoValidation.Endpoints SharpGrip FluentValidation AutoValidation Endpoints @@ -18,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs index f4b0a82..5d4250a 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Extensions/ServiceCollectionExtensions.cs @@ -14,8 +14,7 @@ public static class ServiceCollectionExtensions /// The service collection. /// The configuration delegate used to configure the FluentValidation AutoValidation Endpoints validation. /// The service collection. - public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection, - Action? autoValidationEndpointsConfiguration = null) + public static IServiceCollection AddFluentValidationAutoValidation(this IServiceCollection serviceCollection, Action? autoValidationEndpointsConfiguration = null) { var configuration = new AutoValidationEndpointsConfiguration(); diff --git a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs index 06fdf94..8520683 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Filters/FluentValidationAutoValidationEndpointFilter.cs @@ -2,13 +2,14 @@ using FluentValidation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Results; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters { - public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter + public class FluentValidationAutoValidationEndpointFilter(ILogger logger) : IEndpointFilter { public async ValueTask InvokeAsync(EndpointFilterInvocationContext endpointFilterInvocationContext, EndpointFilterDelegate next) { @@ -18,7 +19,8 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter { if (argument != null && argument.GetType().IsCustomType() && serviceProvider.GetValidator(argument.GetType()) is IValidator validator) { - // ReSharper disable once SuspiciousTypeConversion.Global + logger.LogDebug("Starting validation for argument of type '{Type}'.", argument.GetType().Name); + var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); @@ -26,37 +28,51 @@ public class FluentValidationAutoValidationEndpointFilter : IEndpointFilter if (validatorInterceptor != null) { - validationContext = validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext) ?? validationContext; + logger.LogDebug("Invoking validator interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name); + validationContext = await validatorInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } if (globalValidationInterceptor != null) { - validationContext = globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext) ?? validationContext; + logger.LogDebug("Invoking global validation interceptor BeforeValidation for argument '{Argument}'.", argument.GetType().Name); + validationContext = await globalValidationInterceptor.BeforeValidation(endpointFilterInvocationContext, validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationContext; } var validationResult = await validator.ValidateAsync(validationContext, endpointFilterInvocationContext.HttpContext.RequestAborted); if (validatorInterceptor != null) { - validationResult = validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext) ?? validationResult; + logger.LogDebug("Invoking validator interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name); + validationResult = await validatorInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (globalValidationInterceptor != null) { - validationResult = globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext) ?? validationResult; + logger.LogDebug("Invoking global validation interceptor AfterValidation for argument '{Argument}'.", argument.GetType().Name); + validationResult = await globalValidationInterceptor.AfterValidation(endpointFilterInvocationContext, validationContext, validationResult, endpointFilterInvocationContext.HttpContext.RequestAborted) ?? validationResult; } if (!validationResult.IsValid) { + logger.LogDebug("Validation result not valid for argument '{Argument}': {ErrorCount} validation error(s) found.", argument.GetType().Name, validationResult.Errors.Count); + var fluentValidationAutoValidationResultFactory = serviceProvider.GetService(); + logger.LogDebug("Creating result for path '{Path}'.", endpointFilterInvocationContext.HttpContext.Request.Path); + if (fluentValidationAutoValidationResultFactory != null) { + logger.LogTrace("Creating result for path '{Path}' using a custom result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + return fluentValidationAutoValidationResultFactory.CreateResult(endpointFilterInvocationContext, validationResult); } + logger.LogTrace("Creating result for path '{Path}' using the default result factory.", endpointFilterInvocationContext.HttpContext.Request.Path); + return new FluentValidationAutoValidationDefaultResultFactory().CreateResult(endpointFilterInvocationContext, validationResult); } + + logger.LogDebug("Validation result valid for argument '{Argument}'.", argument.GetType().Name); } } diff --git a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs index 6cc2cd7..13cbc6a 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IGlobalValidationInterceptor.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Http; @@ -11,7 +13,27 @@ namespace SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors /// public interface IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); + /// + /// Executes custom logic before the validation process. Allows intercepting and altering the validation context prior to the execution of the validation rules. + /// + /// The context of the currently executing endpoint filter, providing access to details about the HTTP request and endpoint. + /// The validation context containing information about the object being validated. + /// A token to monitor for cancellation requests. + /// + /// A transformed or new instance to be used in the validation process, or null if no changes need to be applied. + /// + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + + /// + /// Executes custom logic after the validation process. Allows intercepting and altering the validation result or performing additional operations after the validation has been completed. + /// + /// The context of the currently executing endpoint filter, providing access to details about the HTTP request and endpoint. + /// The validation context containing information about the object that was validated. + /// The result of the validation process, including validation errors if any exist. + /// A token to monitor for cancellation requests. + /// + /// A modified or new instance, or null if no changes are required to the validation result. + /// + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs index 28c70e1..0896c1f 100644 --- a/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs +++ b/FluentValidation.AutoValidation.Endpoints/src/Interceptors/IValidatorInterceptor.cs @@ -5,7 +5,5 @@ /// /// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated. /// - public interface IValidatorInterceptor : IGlobalValidationInterceptor - { - } + public interface IValidatorInterceptor : IGlobalValidationInterceptor; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj index 4117c6f..5dd67ba 100644 --- a/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj +++ b/FluentValidation.AutoValidation.Mvc/FluentValidation.AutoValidation.Mvc.csproj @@ -5,7 +5,6 @@ - netcoreapp3.1;net6.0;net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Mvc SharpGrip.FluentValidation.AutoValidation.Mvc SharpGrip FluentValidation AutoValidation MVC @@ -18,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs index 173d93d..c866642 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateAlwaysAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Parameter)] - public class AutoValidateAlwaysAttribute : Attribute - { - } + public class AutoValidateAlwaysAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs index 186e61f..dce6a53 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidateNeverAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter)] - public class AutoValidateNeverAttribute : Attribute - { - } + public class AutoValidateNeverAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs index 799ba08..eaaf12c 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Attributes/AutoValidationAttribute.cs @@ -3,7 +3,5 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class AutoValidationAttribute : Attribute - { - } + public class AutoValidationAttribute : Attribute; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs b/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs deleted file mode 100644 index 666edde..0000000 --- a/FluentValidation.AutoValidation.Mvc/src/Attributes/FluentValidationAutoValidationAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes -{ - [Obsolete("Attribute is obsolete and will be removed in v2. Use the [AutoValidation] attribute instead.")] - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class FluentValidationAutoValidationAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs index 0d86cc3..33023ad 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Configuration/AutoValidationMvcConfiguration.cs @@ -16,7 +16,7 @@ public class AutoValidationMvcConfiguration /// /// Configures the validation strategy. Validation strategy enables asynchronous automatic validation on all controllers inheriting from . - /// Validation strategy enables asynchronous automatic validation on controllers inheriting from decorated (class or method) with a attribute. + /// Validation strategy enables asynchronous automatic validation on controllers decorated (class or method) with a attribute. /// public ValidationStrategy ValidationStrategy { get; set; } = ValidationStrategy.All; @@ -30,7 +30,7 @@ public class AutoValidationMvcConfiguration /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromForm] attribute). /// /// - public bool EnableFormBindingSourceAutomaticValidation { get; set; } = false; + public bool EnableFormBindingSourceAutomaticValidation { get; set; } = true; /// /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromQuery] attribute). @@ -42,7 +42,7 @@ public class AutoValidationMvcConfiguration /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromRoute] attribute). /// /// - public bool EnablePathBindingSourceAutomaticValidation { get; set; } = false; + public bool EnablePathBindingSourceAutomaticValidation { get; set; } = true; /// /// Enables asynchronous automatic validation for parameters bound from binding sources (typically parameters decorated with the [FromHeader] attribute). @@ -70,7 +70,7 @@ public class AutoValidationMvcConfiguration /// The default result factory returns the default object wrapped in a >. /// /// - /// The custom result factory implement . + /// The custom result factory implementing . public void OverrideDefaultResultFactoryWith() where TResultFactory : IFluentValidationAutoValidationResultFactory { OverriddenResultFactory = typeof(TResultFactory); diff --git a/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs b/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs index afdd392..047de58 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Enums/ValidationStrategy.cs @@ -11,7 +11,7 @@ public enum ValidationStrategy All = 1, /// - /// Enables asynchronous automatic validation on controllers inheriting from decorated with a attribute. + /// Enables asynchronous automatic validation on controllers inheriting from decorated with a attribute. /// Annotations = 2 } diff --git a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs index 9f2cc79..1235b6b 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Extensions/ServiceCollectionExtensions.cs @@ -34,11 +34,7 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService if (configuration.DisableBuiltInModelValidation) { - serviceCollection.AddSingleton(serviceProvider => - new FluentValidationAutoValidationObjectModelValidator( - serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, - configuration.DisableBuiltInModelValidation)); + serviceCollection.AddSingleton(serviceProvider => new FluentValidationAutoValidationObjectModelValidator(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService>().Value.ModelValidatorProviders, configuration.DisableBuiltInModelValidation)); } // Add the default result factory. @@ -51,7 +47,7 @@ public static IServiceCollection AddFluentValidationAutoValidation(this IService } // Create a default instance of the `ModelStateInvalidFilter` to access the non static property `Order` in a static context. - var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions {InvalidModelStateResponseFactory = context => new OkResult()}, NullLogger.Instance); + var modelStateInvalidFilter = new ModelStateInvalidFilter(new ApiBehaviorOptions {InvalidModelStateResponseFactory = _ => new OkResult()}, NullLogger.Instance); // Make sure we insert the `FluentValidationAutoValidationActionFilter` before the built-in `ModelStateInvalidFilter` to prevent it short-circuiting the request. serviceCollection.Configure(options => options.Filters.Add(modelStateInvalidFilter.Order - 1)); diff --git a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs index c6a7d79..98e94a0 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Filters/FluentValidationAutoValidationActionFilter.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -9,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; @@ -19,30 +22,25 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Filters { - public class FluentValidationAutoValidationActionFilter : IAsyncActionFilter + public class FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions autoValidationMvcConfiguration, ILogger logger) : IAsyncActionFilter { - private readonly IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory; - private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration; - - public FluentValidationAutoValidationActionFilter(IFluentValidationAutoValidationResultFactory fluentValidationAutoValidationResultFactory, IOptions autoValidationMvcConfiguration) - { - this.fluentValidationAutoValidationResultFactory = fluentValidationAutoValidationResultFactory; - this.autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value; - } + private readonly AutoValidationMvcConfiguration autoValidationMvcConfiguration = autoValidationMvcConfiguration.Value; public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingContext, ActionExecutionDelegate next) { + var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; + + logger.LogDebug("Starting validation for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + if (IsValidController(actionExecutingContext.Controller)) { var endpoint = actionExecutingContext.HttpContext.GetEndpoint(); - var controllerActionDescriptor = (ControllerActionDescriptor) actionExecutingContext.ActionDescriptor; var serviceProvider = actionExecutingContext.HttpContext.RequestServices; - if (endpoint != null && - ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && - !endpoint.Metadata.OfType().Any() && !endpoint.Metadata.OfType().Any()) || - endpoint.Metadata.OfType().Any())) + if (endpoint != null && ((autoValidationMvcConfiguration.ValidationStrategy == ValidationStrategy.Annotations && !endpoint.Metadata.OfType().Any()) || endpoint.Metadata.OfType().Any())) { + logger.LogDebug("Skipping validation for action '{Action}' on controller '{Controller}' due to validation strategy or AutoValidateNeverAttribute.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + HandleUnvalidatedEntries(actionExecutingContext); await next(); @@ -50,6 +48,8 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC return; } + var validationResults = new Dictionary(); + foreach (var parameter in controllerActionDescriptor.Parameters) { if (actionExecutingContext.ActionArguments.TryGetValue(parameter.Name, out var subject)) @@ -61,10 +61,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC var hasAutoValidateAlwaysAttribute = parameterInfo?.HasCustomAttribute() ?? false; var hasAutoValidateNeverAttribute = parameterInfo?.HasCustomAttribute() ?? false; - if (subject != null && parameterType != null && parameterType.IsCustomType() && - !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && - serviceProvider.GetValidator(parameterType) is IValidator validator) + if (subject != null && parameterType != null && parameterType.IsCustomType() && !hasAutoValidateNeverAttribute && (hasAutoValidateAlwaysAttribute || HasValidBindingSource(bindingSource)) && serviceProvider.GetValidator(parameterType) is IValidator validator) { + logger.LogDebug("Validating parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + // ReSharper disable once SuspiciousTypeConversion.Global var validatorInterceptor = validator as IValidatorInterceptor; var globalValidationInterceptor = serviceProvider.GetService(); @@ -73,33 +73,46 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (validatorInterceptor != null) { - validationContext = validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + logger.LogDebug("Invoking validator interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name); + validationContext = await validatorInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } if (globalValidationInterceptor != null) { - validationContext = globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; + logger.LogDebug("Invoking global validation interceptor BeforeValidation for parameter '{Parameter}'.", parameter.Name); + validationContext = await globalValidationInterceptor.BeforeValidation(actionExecutingContext, validationContext) ?? validationContext; } var validationResult = await validator.ValidateAsync(validationContext, actionExecutingContext.HttpContext.RequestAborted); + validationResults.Add(validationContext, validationResult); if (validatorInterceptor != null) { - validationResult = validatorInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + logger.LogDebug("Invoking validator interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name); + validationResult = await validatorInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (globalValidationInterceptor != null) { - validationResult = globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext) ?? validationResult; + logger.LogDebug("Invoking global validation interceptor AfterValidation for parameter '{Parameter}'.", parameter.Name); + validationResult = await globalValidationInterceptor.AfterValidation(actionExecutingContext, validationContext, validationResult) ?? validationResult; } if (!validationResult.IsValid) { + logger.LogDebug("Validation result not valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}': {ErrorCount} validation error(s) found.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName, validationResult.Errors.Count); + foreach (var error in validationResult.Errors) { + logger.LogTrace("Adding validation error '{ErrorMessage}' for '{ParameterName}' to ModelState.", error.ErrorMessage, parameter.Name); + actionExecutingContext.ModelState.AddModelError(error.PropertyName, error.ErrorMessage); } } + else + { + logger.LogDebug("Validation result valid for parameter '{Parameter}' of type '{Type}' for action '{Action}' on controller '{Controller}'.", parameter.Name, parameterType.Name, controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + } } } } @@ -108,13 +121,26 @@ public async Task OnActionExecutionAsync(ActionExecutingContext actionExecutingC if (!actionExecutingContext.ModelState.IsValid) { + logger.LogDebug("ModelState is not valid for action '{Action}' on controller '{Controller}'. Creating validation problem details.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + var problemDetailsFactory = serviceProvider.GetRequiredService(); var validationProblemDetails = problemDetailsFactory.CreateValidationProblemDetails(actionExecutingContext.HttpContext, actionExecutingContext.ModelState); - actionExecutingContext.Result = fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails); + logger.LogTrace("Creating action result for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); - return; + actionExecutingContext.Result = await fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults); + + if (actionExecutingContext.Result != null) + { + logger.LogTrace("Action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); + + return; + } + + logger.LogTrace("No action result created for action '{Action}' on controller '{Controller}'.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); } + + logger.LogDebug("ModelState is valid for action '{Action}' on controller '{Controller}'. Proceeding with action execution.", controllerActionDescriptor.ActionName, controllerActionDescriptor.ControllerName); } await next(); @@ -126,13 +152,12 @@ private bool IsValidController(object controller) if (controllerType.HasCustomAttribute()) { + logger.LogDebug("Controller '{Controller}' is marked with NonControllerAttribute. Skipping validation.", controllerType.Name); + return false; } - return controller is ControllerBase || - controllerType.HasCustomAttribute() || - controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || - controllerType.InheritsFromTypeWithNameEndingIn("Controller"); + return controller is ControllerBase || controllerType.HasCustomAttribute() || controllerType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || controllerType.InheritsFromTypeWithNameEndingIn("Controller"); } private bool HasValidBindingSource(BindingSource? bindingSource) @@ -150,11 +175,17 @@ private void HandleUnvalidatedEntries(ActionExecutingContext context) { if (autoValidationMvcConfiguration.DisableBuiltInModelValidation) { + logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to true."); + foreach (var modelStateEntry in context.ModelState.Values.Where(modelStateEntry => modelStateEntry.ValidationState == ModelValidationState.Unvalidated)) { modelStateEntry.ValidationState = ModelValidationState.Skipped; } } + else + { + logger.LogDebug("Skipping validation of unvalidated entries due to DisableBuiltInModelValidation being set to false."); + } } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs index 8c88e59..b4197f1 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IGlobalValidationInterceptor.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Mvc.Filters; @@ -11,7 +13,27 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Interceptors /// public interface IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); + /// + /// Executes custom logic before the validation process. Allows intercepting and altering the validation context prior to the execution of the validation rules. + /// + /// The context of the currently executing action, providing access to details about the HTTP request and action. + /// The validation context containing information about the object being validated. + /// A token to monitor for cancellation requests. + /// + /// A transformed or new instance to be used in the validation process, or null if no changes need to be applied. + /// + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + + /// + /// Executes custom logic after the validation process. Allows intercepting and altering the validation result or performing additional operations after the validation has been completed. + /// + /// The context of the currently executing action, providing access to details about the HTTP request and action. + /// The validation context containing information about the object that was validated. + /// The result of the validation process, including validation errors if any exist. + /// A token to monitor for cancellation requests. + /// + /// A modified or new instance, or null if no changes are required to the validation result. + /// + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs index 92d8d97..3414d5f 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Interceptors/IValidatorInterceptor.cs @@ -5,7 +5,5 @@ /// /// The interceptor methods of instances of this interface will only get called when the implementing validator gets validated. /// - public interface IValidatorInterceptor : IGlobalValidationInterceptor - { - } + public interface IValidatorInterceptor : IGlobalValidationInterceptor; } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs b/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs index bef32b9..9a6a899 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Results/FluentValidationAutoValidationDefaultResultFactory.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Results { public class FluentValidationAutoValidationDefaultResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new BadRequestObjectResult(validationProblemDetails); + return Task.FromResult(new BadRequestObjectResult(validationProblemDetails)); } } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs b/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs index a106f1a..42da4ab 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Results/IFluentValidationAutoValidationResultFactory.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Results @@ -10,7 +14,8 @@ public interface IFluentValidationAutoValidationResultFactory /// /// The associated with the current request/response. /// The instance object containing the validation failures. - /// The object to be executed by the controller action. - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails); + /// The dictionary of instances keyed by the models containing the validation results from all validators that were executed. + /// The object to be executed by the controller action or null to prevent short-circuiting the action. + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults); } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs index 6c5c6b6..31104f4 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationObjectModelValidator.cs @@ -5,21 +5,9 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { - public class FluentValidationAutoValidationObjectModelValidator : ObjectModelValidator + public class FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, bool disableBuiltInModelValidation) : ObjectModelValidator(modelMetadataProvider, validatorProviders) { - private readonly bool disableBuiltInModelValidation; - - public FluentValidationAutoValidationObjectModelValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, bool disableBuiltInModelValidation) - : base(modelMetadataProvider, validatorProviders) - { - this.disableBuiltInModelValidation = disableBuiltInModelValidation; - } - - public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, - IModelValidatorProvider validatorProvider, - ValidatorCache validatorCache, - IModelMetadataProvider metadataProvider, - ValidationStateDictionary? validationState) + public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState) { return new FluentValidationAutoValidationValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState, disableBuiltInModelValidation); } diff --git a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs index b72f240..22f54ff 100644 --- a/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs +++ b/FluentValidation.AutoValidation.Mvc/src/Validation/FluentValidationAutoValidationValidationVisitor.cs @@ -4,33 +4,12 @@ namespace SharpGrip.FluentValidation.AutoValidation.Mvc.Validation { - public class FluentValidationAutoValidationValidationVisitor : ValidationVisitor + public class FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary? validationState, bool disableBuiltInModelValidation) : ValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { - private readonly bool disableBuiltInModelValidation; - - public FluentValidationAutoValidationValidationVisitor(ActionContext actionContext, - IModelValidatorProvider validatorProvider, - ValidatorCache validatorCache, - IModelMetadataProvider metadataProvider, - ValidationStateDictionary? validationState, - bool disableBuiltInModelValidation) - : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) - { - this.disableBuiltInModelValidation = disableBuiltInModelValidation; - } - - public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel) - { - // If built in model validation is disabled return true for later validation in the action filter. - return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel); - } - -#if !NETCOREAPP3_1 public override bool Validate(ModelMetadata? metadata, string? key, object? model, bool alwaysValidateAtTopLevel, object? container) { // If built in model validation is disabled return true for later validation in the action filter. return disableBuiltInModelValidation || base.Validate(metadata, key, model, alwaysValidateAtTopLevel, container); } -#endif } } \ No newline at end of file diff --git a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj index eed5b58..2ed14a7 100644 --- a/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj +++ b/FluentValidation.AutoValidation.Shared/FluentValidation.AutoValidation.Shared.csproj @@ -5,7 +5,6 @@ - netstandard2.0;netstandard2.1;netcoreapp3.1;net6.0;net7.0;net8.0;net9.0 SharpGrip.FluentValidation.AutoValidation.Shared SharpGrip.FluentValidation.AutoValidation.Shared SharpGrip FluentValidation AutoValidation Shared @@ -19,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs index f8b08ca..019bd3c 100644 --- a/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs +++ b/FluentValidation.AutoValidation.Shared/src/Extensions/TypeExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; namespace SharpGrip.FluentValidation.AutoValidation.Shared.Extensions @@ -7,18 +9,38 @@ public static class TypeExtensions { public static bool IsCustomType(this Type? type) { - var builtInTypes = new[] + if (type == null || type.IsEnum || type.IsPrimitive) + { + return false; + } + + var builtInTypes = new HashSet { typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), + typeof(DateOnly), + typeof(TimeOnly), + typeof(Uri), typeof(Guid), typeof(Enum) }; - return type != null && type.IsClass && !type.IsEnum && !type.IsValueType && !type.IsPrimitive && !builtInTypes.Contains(type); + if (builtInTypes.Contains(type)) + { + return false; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + var underlyingType = type.IsArray ? type.GetElementType() : type.GetGenericArguments().LastOrDefault(); + + return underlyingType != null && underlyingType.IsCustomType(); + } + + return type.IsClass || type.IsValueType; } public static bool HasCustomAttribute(this Type type) where TAttribute : Attribute diff --git a/README.md b/README.md index cf29fe7..a6de430 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=SharpGrip_FluentValidation.AutoValidation&metric=security_rating)](https://sonarcloud.io/summary/overall?id=SharpGrip_FluentValidation.AutoValidation) \ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=SharpGrip_FluentValidation.AutoValidation&metric=coverage)](https://sonarcloud.io/summary/overall?id=SharpGrip_FluentValidation.AutoValidation) +## Upgrading + +Check out the [upgrade guide](UPGRADING.md). + ## Introduction SharpGrip FluentValidation AutoValidation is an extension of the [FluentValidation](https://github.com/FluentValidation/FluentValidation) (v10+) library enabling automatic asynchronous validation in MVC controllers and minimal APIs (endpoints). @@ -72,17 +76,17 @@ app.MapPost("/", (SomeOtherModel someOtherModel) => $"Hello again {someOtherMode ### MVC controllers -| Property | Default value | Description | -|----------------------------------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DisableBuiltInModelValidation | `false` | Disables the built-in .NET model (data annotations) validation. | -| ValidationStrategy | `ValidationStrategy.All` | Configures the validation strategy. Validation strategy `ValidationStrategy.All` enables asynchronous automatic validation on all controllers inheriting from `ControllerBase`. Validation strategy `ValidationStrategy.Annotations` enables asynchronous automatic validation on controllers inheriting from `ControllerBase` decorated (class or method) with a `[AutoValidationAttribute]` attribute. | -| EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Body` binding sources (typically parameters decorated with the `[FromBody]` attribute). | -| EnableFormBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | -| EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Query` binding sources (typically parameters decorated with the `[FromQuery]` attribute). | -| EnablePathBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | -| EnableHeaderBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Header` binding sources (typically parameters decorated with the `[FromHeader]` attribute). | -| EnableCustomBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Custom` binding sources. | -| EnableNullBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters not bound from any binding source (typically parameters without a declared or inferred binding source). | +| Property | Default value | Description | +|----------------------------------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DisableBuiltInModelValidation | `false` | Disables the built-in .NET model (data annotations) validation. | +| ValidationStrategy | `ValidationStrategy.All` | Configures the validation strategy. Validation strategy `ValidationStrategy.All` enables asynchronous automatic validation on all controllers. Validation strategy `ValidationStrategy.Annotations` enables asynchronous automatic validation on controllers decorated (class or method) with a `[AutoValidationAttribute]` attribute. | +| EnableBodyBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Body` binding sources (typically parameters decorated with the `[FromBody]` attribute). | +| EnableFormBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Form` binding sources (typically parameters decorated with the `[FromForm]` attribute). | +| EnableQueryBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Query` binding sources (typically parameters decorated with the `[FromQuery]` attribute). | +| EnablePathBindingSourceAutomaticValidation | `true` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Path` binding sources (typically parameters decorated with the `[FromRoute]` attribute). | +| EnableHeaderBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Header` binding sources (typically parameters decorated with the `[FromHeader]` attribute). | +| EnableCustomBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters bound from `BindingSource.Custom` binding sources. | +| EnableNullBindingSourceAutomaticValidation | `false` | Enables asynchronous automatic validation for parameters not bound from any binding source (typically parameters without a declared or inferred binding source). | ```cs using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; @@ -116,7 +120,7 @@ builder.Services.AddFluentValidationAutoValidation(configuration => public class CustomResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { return new BadRequestObjectResult(new {Title = "Validation errors", ValidationErrors = validationProblemDetails?.Errors}); } @@ -171,13 +175,13 @@ Implement the `IValidatorInterceptor` interface directly on a specific validator In the validation process, both the global and the validator interceptors are resolved and invoked (if they exist), thereby establishing a miniature pipeline of validation interceptors: ``` -==> IValidatorInterceptor.BeforeValidation() -==> IGlobalValidationInterceptor.BeforeValidation() +IValidatorInterceptor.BeforeValidation() +IGlobalValidationInterceptor.BeforeValidation() -Validation +>> Validation << -==> IValidatorInterceptor.AfterValidation() -==> IGlobalValidationInterceptor.AfterValidation() +IValidatorInterceptor.AfterValidation() +IGlobalValidationInterceptor.AfterValidation() ``` Both interfaces define a `BeforeValidation` and a `AfterValidation` method. @@ -186,7 +190,7 @@ The `BeforeValidation` method gets called before validation and allows you to re In case you return `null` the default `IValidationContext` will be passed to the validator. The `AfterValidation` method gets called after validation and allows you to return a custom `IValidationResult` which gets passed to the `IFluentValidationAutoValidationResultFactory`. -In case you return `null` the default `IValidationResult` will be passed to the `IFluentValidationAutoValidationResultFactory`. +In case you return `null` the existing `ValidationResult` will be passed to the `IFluentValidationAutoValidationResultFactory`. ### MVC controllers @@ -196,16 +200,16 @@ builder.Services.AddTransient BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } @@ -219,16 +223,16 @@ private class TestValidator : AbstractValidator, IValidatorIntercepto RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } ``` @@ -241,16 +245,16 @@ builder.Services.AddTransient BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } @@ -264,16 +268,16 @@ private class TestValidator : AbstractValidator, IValidatorIntercepto RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { // Return a custom `IValidationContext` or null. - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { // Return a custom `ValidationResult` or null. - return null; + return Task.FromResult(null); } } ``` diff --git a/Tests/FluentValidation.AutoValidation.Tests.csproj b/Tests/FluentValidation.AutoValidation.Tests.csproj index 0f5fe11..8f03e9b 100644 --- a/Tests/FluentValidation.AutoValidation.Tests.csproj +++ b/Tests/FluentValidation.AutoValidation.Tests.csproj @@ -1,33 +1,32 @@ - net7.0;net8.0;net9.0 latest false SharpGrip.FluentValidation.AutoValidation.Tests - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs index 78bf5d1..d5edb2c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,4 +1,6 @@ -using FluentValidation.Results; +// ReSharper disable InconsistentNaming + +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs index a780300..1836bab 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Filters/FluentValidationAutoValidationEndpointFilterTest.cs @@ -1,11 +1,15 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Logging; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Filters; using SharpGrip.FluentValidation.AutoValidation.Endpoints.Interceptors; @@ -25,6 +29,7 @@ public class FluentValidationAutoValidationEndpointFilterTest [Fact] public async Task TestInvokeAsync_ValidatorFound() { + var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); @@ -35,7 +40,7 @@ public async Task TestInvokeAsync_ValidatorFound() var validationFailuresValues = ValidationFailures.Values.ToList(); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); var result = (ValidationProblem) (await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!))!; var problemDetailsErrorValues = result.ProblemDetails.Errors.ToList(); @@ -48,6 +53,7 @@ public async Task TestInvokeAsync_ValidatorFound() [Fact] public async Task TestInvokeAsync_ValidatorNotFound() { + var logger = Substitute.For>(); var serviceProvider = Substitute.For(); var endpointFilterInvocationContext = Substitute.For(); @@ -56,7 +62,7 @@ public async Task TestInvokeAsync_ValidatorNotFound() serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(typeof(TestModel))).Returns(null); serviceProvider.GetService(typeof(IGlobalValidationInterceptor)).Returns(null); - var endpointFilter = new FluentValidationAutoValidationEndpointFilter(); + var endpointFilter = new FluentValidationAutoValidationEndpointFilter(logger); var result = await endpointFilter.InvokeAsync(endpointFilterInvocationContext, _ => ValueTask.FromResult(new object())!); @@ -79,27 +85,27 @@ public TestValidator() RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } private class GlobalValidationInterceptor : IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext) + public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs b/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs index 5341b31..9fd3aed 100644 --- a/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Endpoints/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs index 2df849c..2ffc6de 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Configuration/AutoValidationMvcConfigurationTest.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; using SharpGrip.FluentValidation.AutoValidation.Mvc.Results; @@ -24,9 +28,9 @@ public void TestOverrideDefaultResultFactoryWith() private class TestResultFactory : IFluentValidationAutoValidationResultFactory { - public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails) + public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new OkResult(); + return Task.FromResult(new OkResult()); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs index 426c999..532d583 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Extensions/ServiceCollectionExtensionsTest.cs @@ -1,4 +1,10 @@ -using Microsoft.AspNetCore.Mvc; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; @@ -86,9 +92,9 @@ private static void AssertNotContainsServiceDescriptor CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults) { - return new OkResult(); + return Task.FromResult(new OkResult()); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs index ea156f0..93e85c3 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Filters/FluentValidationAutoValidationActionFilterTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Mvc.Configuration; @@ -37,7 +39,7 @@ public async Task TestOnActionExecutionAsync() Parameter2 = "Value 2", Parameter3 = "Value 3" } - }, + } }; var controllerActionDescriptor = new ControllerActionDescriptor { @@ -65,6 +67,7 @@ public async Task TestOnActionExecutionAsync() var problemDetailsFactory = Substitute.For(); var fluentValidationAutoValidationResultFactory = Substitute.For(); var autoValidationMvcConfiguration = Substitute.For>(); + var logger = Substitute.For>(); var httpContext = Substitute.For(); var controller = Substitute.For(); var actionContext = Substitute.For(httpContext, Substitute.For(), controllerActionDescriptor, modelStateDictionary); @@ -76,14 +79,14 @@ public async Task TestOnActionExecutionAsync() serviceProvider.GetService(typeof(ProblemDetailsFactory)).Returns(problemDetailsFactory); problemDetailsFactory.CreateValidationProblemDetails(httpContext, modelStateDictionary).Returns(validationProblemDetails); - fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails)); + fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, Arg.Any>()).Returns(new BadRequestObjectResult(validationProblemDetails)); httpContext.RequestServices.Returns(serviceProvider); actionExecutingContext.Controller.Returns(controller); actionExecutingContext.ActionDescriptor = controllerActionDescriptor; actionExecutingContext.ActionArguments.Returns(actionArguments); autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); - var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration); + var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration, logger); await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext)); @@ -129,7 +132,7 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT { Parameters = [ - new() + new ParameterDescriptor { Name = "request", ParameterType = typeof(CreateAnimalRequest), @@ -156,12 +159,14 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT var actionExecutedContext = Substitute.For(actionContext, new List(), new object()); var fluentValidationAutoValidationResultFactory = Substitute.For(); - fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails).Returns(new BadRequestObjectResult(validationProblemDetails)); + fluentValidationAutoValidationResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, Arg.Any>()).Returns(new BadRequestObjectResult(validationProblemDetails)); var autoValidationMvcConfiguration = Substitute.For>(); autoValidationMvcConfiguration.Value.Returns(new AutoValidationMvcConfiguration()); - var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration); + var logger = Substitute.For>(); + + var actionFilter = new FluentValidationAutoValidationActionFilter(fluentValidationAutoValidationResultFactory, autoValidationMvcConfiguration, logger); // Act await actionFilter.OnActionExecutionAsync(actionExecutingContext, () => Task.FromResult(actionExecutedContext)); @@ -169,46 +174,35 @@ public async Task OnActionExecutionAsync_WithInstanceTypeDifferentThanParameterT // Assert var modelStateDictionaryValues = modelStateDictionary.Values.ToList(); var validationFailuresValues = validationFailures.Values.ToList(); - var badRequestObjectResult = (BadRequestObjectResult)actionExecutingContext.Result!; - var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails)badRequestObjectResult.Value!; + var badRequestObjectResult = (BadRequestObjectResult) actionExecutingContext.Result!; + var badRequestObjectResultValidationProblemDetails = (ValidationProblemDetails) badRequestObjectResult.Value!; Assert.Contains(validationFailuresValues[0].First(), modelStateDictionaryValues[0].Errors.Select(error => error.ErrorMessage)); Assert.Contains(validationFailuresValues[0].First(), badRequestObjectResultValidationProblemDetails.Errors[nameof(CreatePersonRequest.Name)][0]); } - public class AnimalsController : ControllerBase - { - } + public class AnimalsController : ControllerBase; - public class CreateAnimalRequest - { - } + public class CreateAnimalRequest; public class CreatePersonRequest : CreateAnimalRequest { public required string Name { get; set; } } - public class CreateAnimalRequestValidator : AbstractValidator - { - public CreateAnimalRequestValidator() - { - } - } + public class CreateAnimalRequestValidator : AbstractValidator; public class CreatePersonRequestValidator : AbstractValidator { public CreatePersonRequestValidator() { - this.Include(new CreateAnimalRequestValidator()); + Include(new CreateAnimalRequestValidator()); - this.RuleFor(x => x.Name).Equal("John Doe"); + RuleFor(x => x.Name).Equal("John Doe"); } } - public class TestController : ControllerBase - { - } + public class TestController : ControllerBase; private class TestModel { @@ -226,27 +220,27 @@ public TestValidator() RuleFor(x => x.Parameter3).Empty(); } - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } private class GlobalValidationInterceptor : IGlobalValidationInterceptor { - public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } - public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext) + public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default) { - return null; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs index 4f264a0..8ab8de6 100644 --- a/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Mvc/Results/FluentValidationAutoValidationDefaultResultFactoryTest.cs @@ -1,4 +1,9 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -14,13 +19,13 @@ public class FluentValidationAutoValidationDefaultResultFactoryTest { private static readonly Dictionary ValidationFailures = new() { - {"Property 1", new[] {"Error message 1"}}, - {"Property 2", new[] {"Error message 2"}}, - {"Property 3", new[] {"Error message 3"}}, + {"Property 1", ["Error message 1"]}, + {"Property 2", ["Error message 2"]}, + {"Property 3", ["Error message 3"]}, }; [Fact] - public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuiltInModelValidation_False() + public async Task TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuiltInModelValidation_False() { var fluentValidationAutoValidationDefaultResultFactory = new FluentValidationAutoValidationDefaultResultFactory(); @@ -29,9 +34,10 @@ public void TestAddFluentValidationAutoValidation_WithConfiguration_DisableBuilt var validationProblemDetails = new ValidationProblemDetails(ValidationFailures); var badRequestObjectResult = new BadRequestObjectResult(validationProblemDetails); + var validationResults = new Dictionary(); - var resultFactoryResult = (BadRequestObjectResult) fluentValidationAutoValidationDefaultResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails); + var resultFactoryResult = (BadRequestObjectResult?) await fluentValidationAutoValidationDefaultResultFactory.CreateActionResult(actionExecutingContext, validationProblemDetails, validationResults); - Assert.Equal(badRequestObjectResult.Value, resultFactoryResult.Value); + Assert.Equal(badRequestObjectResult.Value, resultFactoryResult?.Value); } } \ No newline at end of file diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs index e455f75..dd60a3c 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ServiceProviderExtensionsTest.cs @@ -1,4 +1,6 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; using FluentValidation; using NSubstitute; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs index bfbd3d6..d99daa0 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/TypeExtensionsTest.cs @@ -1,4 +1,7 @@ -using System; +// ReSharper disable InconsistentNaming + +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; @@ -13,6 +16,7 @@ public void Test_IsCustomType() { Assert.True(typeof(TestModelClass).IsCustomType()); Assert.True(typeof(TestModelRecord).IsCustomType()); + Assert.True(typeof(TestModelStruct).IsCustomType()); Assert.False(typeof(TestModelEnum).IsCustomType()); Assert.False(typeof(Enum).IsCustomType()); Assert.False(typeof(string).IsCustomType()); @@ -36,6 +40,88 @@ public void Test_IsCustomType() Assert.False(typeof(Guid).IsCustomType()); Assert.False(typeof(DateOnly).IsCustomType()); Assert.False(typeof(TimeOnly).IsCustomType()); + Assert.False(typeof(Uri).IsCustomType()); + } + + [Fact] + public void Test_IsCustomType_Collections() + { + Assert.True(typeof(ICollection).IsCustomType()); + Assert.True(typeof(ICollection).IsCustomType()); + Assert.True(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + Assert.False(typeof(ICollection).IsCustomType()); + + Assert.True(typeof(IList).IsCustomType()); + Assert.True(typeof(IList).IsCustomType()); + Assert.True(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + Assert.False(typeof(IList).IsCustomType()); + + Assert.True(typeof(List).IsCustomType()); + Assert.True(typeof(List).IsCustomType()); + Assert.True(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + Assert.False(typeof(List).IsCustomType()); + + Assert.True(typeof(TestModelClass[]).IsCustomType()); + Assert.True(typeof(TestModelRecord[]).IsCustomType()); + Assert.True(typeof(TestModelStruct[]).IsCustomType()); + Assert.False(typeof(TestModelEnum[]).IsCustomType()); + Assert.False(typeof(Enum[]).IsCustomType()); + Assert.False(typeof(string[]).IsCustomType()); + + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.True(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + Assert.False(typeof(Dictionary).IsCustomType()); + + Assert.True(typeof(HashSet).IsCustomType()); + Assert.True(typeof(HashSet).IsCustomType()); + Assert.True(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + Assert.False(typeof(HashSet).IsCustomType()); + + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.True(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + Assert.False(typeof(IEnumerable).IsCustomType()); + + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.True(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + Assert.False(typeof(IReadOnlyList).IsCustomType()); + + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.True(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + Assert.False(typeof(IReadOnlyCollection).IsCustomType()); + + Assert.True(typeof(ISet).IsCustomType()); + Assert.True(typeof(ISet).IsCustomType()); + Assert.True(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + Assert.False(typeof(ISet).IsCustomType()); + + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); + Assert.False(typeof(IAsyncEnumerable).IsCustomType()); } [Fact] @@ -68,6 +154,8 @@ private class TestModelClass; [AutoValidateNever] private record TestModelRecord; + private struct TestModelStruct; + private enum TestModelEnum; private class TestInherits1 : Controller; diff --git a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs index 481193e..f5789c7 100644 --- a/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs +++ b/Tests/src/FluentValidation.AutoValidation.Shared/Extensions/ValidationResultExtensionsTest.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// ReSharper disable InconsistentNaming + +using System.Collections.Generic; using System.Linq; using FluentValidation.Results; using SharpGrip.FluentValidation.AutoValidation.Shared.Extensions; diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..c3d6461 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,47 @@ +# SharpGrip FluentValidation AutoValidation + +## Upgrade guide + +### Upgrade from v1 to v2 + +#### Configuration - MVC controllers +The default values of the configuration options below have been changed: + +- `EnableFormBindingSourceAutomaticValidation` from `false` to `true` +- `EnablePathBindingSourceAutomaticValidation` from `false` to `true` + +#### Result factories - MVC controllers + +```diff +- public IActionResult CreateActionResult(ActionExecutingContext context, ValidationProblemDetails? validationProblemDetails); ++ public Task CreateActionResult(ActionExecutingContext context, ValidationProblemDetails validationProblemDetails, IDictionary validationResults); +``` + +#### Validation interceptors - MVC controllers + +```diff +- public IValidationContext? BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); ++ public Task BeforeValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + +- public ValidationResult? AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext); ++ public Task AfterValidation(ActionExecutingContext actionExecutingContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); +``` + +#### Validation interceptors - Minimal APIs (endpoints) + +```diff +- public IValidationContext? BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); ++ public Task BeforeValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, CancellationToken cancellationToken = default); + +- public ValidationResult? AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext); ++ public Task AfterValidation(EndpointFilterInvocationContext endpointFilterInvocationContext, IValidationContext validationContext, ValidationResult validationResult, CancellationToken cancellationToken = default); +``` + +#### Attributes - MVC controllers + +Replace the deprecated `SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.FluentValidationAutoValidationAttribute` with `SharpGrip.FluentValidation.AutoValidation.Mvc.Attributes.AutoValidationAttribute`. + +```diff +- [FluentValidationAutoValidation] ++ [AutoValidation] +``` \ No newline at end of file