From e68dabf59b37c14c56db05696306b3dd163baf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 21 Sep 2017 17:00:34 +0300 Subject: [PATCH] Impelemented validation filter for asp.net core mvc. --- .../AspNetCore/Mvc/AbpMvcOptionsExtensions.cs | 3 +- .../ExceptionHandling/AbpExceptionFilter.cs | 2 +- .../Validation/AbpValidationActionFilter.cs | 42 +++++++++++ .../MvcActionInvocationValidator.cs | 71 +++++++++++++++++++ .../App/ValidationTestController.cs | 34 +++++++++ .../ExceptionTestController_Tests.cs | 2 +- .../ValidationTestController_Tests.cs | 39 ++++++++++ 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs create mode 100644 src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs create mode 100644 test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ValidationTestController.cs rename test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/{ => ExceptionHandling}/ExceptionTestController_Tests.cs (95%) create mode 100644 test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 6a0d7a1d28..53ae0da753 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; +using Volo.Abp.AspNetCore.Mvc.Validation; namespace Volo.Abp.AspNetCore.Mvc { @@ -22,7 +23,7 @@ namespace Volo.Abp.AspNetCore.Mvc { //options.Filters.AddService(typeof(AbpAuthorizationFilter)); //options.Filters.AddService(typeof(AbpAuditActionFilter)); - //options.Filters.AddService(typeof(AbpValidationActionFilter)); + options.Filters.AddService(typeof(AbpValidationActionFilter)); //options.Filters.AddService(typeof(AbpUowActionFilter)); options.Filters.AddService(typeof(AbpExceptionFilter)); //options.Filters.AddService(typeof(AbpResultFilter)); diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs index 0c6eb45380..9498365932 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs @@ -43,7 +43,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling private void HandleAndWrapException(ExceptionContext context) { - if (!ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)) + if (!context.ActionDescriptor.IsControllerAction() || !ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)) { return; } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs new file mode 100644 index 0000000000..949c2e1984 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Aspects; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency + { + private readonly IServiceProvider _serviceProvider; + + public AbpValidationActionFilter(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + //TODO: Configuration to disable validation for controllers..? + if (!context.ActionDescriptor.IsControllerAction() || + !ActionResultHelper.IsObjectResult(context.ActionDescriptor.GetMethodInfo().ReturnType)) + { + await next(); + return; + } + + using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation)) + { + using (var scope = _serviceProvider.CreateScope()) + { + var validator = scope.ServiceProvider.GetRequiredService(); + validator.Initialize(context); + validator.Validate(); + } + + await next(); + } + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs new file mode 100644 index 0000000000..26d1e57847 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/MvcActionInvocationValidator.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using Volo.Abp.Validation; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public class MvcActionInvocationValidator : MethodInvocationValidator + { + protected ActionExecutingContext ActionContext { get; private set; } + + private bool _isValidatedBefore; + + public MvcActionInvocationValidator(IOptions options) + : base(options) + { + + } + + public void Initialize(ActionExecutingContext actionContext) + { + ActionContext = actionContext; + + SetDataAnnotationAttributeErrors(); + + base.Initialize( + actionContext.ActionDescriptor.GetMethodInfo(), + GetParameterValues(actionContext) + ); + } + + protected override void SetDataAnnotationAttributeErrors(object validatingObject) + { + SetDataAnnotationAttributeErrors(); + } + + protected virtual void SetDataAnnotationAttributeErrors() + { + if (_isValidatedBefore || ActionContext.ModelState.IsValid) + { + return; + } + + foreach (var state in ActionContext.ModelState) + { + foreach (var error in state.Value.Errors) + { + ValidationErrors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); + } + } + + _isValidatedBefore = true; + } + + protected virtual object[] GetParameterValues(ActionExecutingContext actionContext) + { + var methodInfo = actionContext.ActionDescriptor.GetMethodInfo(); + + var parameters = methodInfo.GetParameters(); + var parameterValues = new object[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterValues[i] = actionContext.ActionArguments.GetOrDefault(parameters[i].Name); + } + + return parameterValues; + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ValidationTestController.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ValidationTestController.cs new file mode 100644 index 0000000000..39107af34f --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/ValidationTestController.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Shouldly; +using Volo.Abp.AspNetCore.Mvc; + +namespace Volo.Abp.AspNetCore.App +{ + [Route("api/validation-test")] + public class ValidationTestController : AbpController + { + [HttpGet] + [Route("object-result-action")] + public Task ObjectResultAction(ValidationTest1Model model) + { + ModelState.IsValid.ShouldBeTrue(); //AbpValidationFilter throws exception otherwise + return Task.FromResult(model.Value1); + } + + [HttpGet] + [Route("action-result-action")] + public IActionResult ActionResultAction(ValidationTest1Model model) + { + return Content("ModelState.IsValid: " + ModelState.IsValid.ToString().ToLowerInvariant()); + } + + public class ValidationTest1Model + { + [Required] + [MinLength(2)] + public string Value1 { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs similarity index 95% rename from test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs rename to test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs index 2ad1e7b447..11ba759b30 100644 --- a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionTestController_Tests.cs +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs @@ -5,7 +5,7 @@ using Volo.Abp.Http; using Volo.Abp.Ui; using Xunit; -namespace Volo.Abp.AspNetCore.Mvc +namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { public class ExceptionTestController_Tests : AspNetCoreMvcTestBase { diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs new file mode 100644 index 0000000000..fb9381babc --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Validation/ValidationTestController_Tests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Http; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Validation +{ + public class ValidationTestController_Tests : AspNetCoreMvcTestBase + { + [Fact] + public async Task Should_Validate_Object_Result_Success() + { + var result = await GetResponseAsStringAsync("/api/validation-test/object-result-action?value1=hello"); + result.ShouldBe("hello"); + } + + [Fact] + public async Task Should_Validate_Object_Result_Failing() + { + var result = await GetResponseAsObjectAsync("/api/validation-test/object-result-action?value1=a", HttpStatusCode.BadRequest); //value1 has min length of 2 chars. + result.Error.ValidationErrors.Length.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Not_Validate_Action_Result_Success() + { + var result = await GetResponseAsStringAsync("/api/validation-test/action-result-action?value1=hello"); + result.ShouldBe("ModelState.IsValid: true"); + } + + [Fact] + public async Task Should_Not_Validate_Action_Result_Failing() + { + var result = await GetResponseAsStringAsync("/api/validation-test/action-result-action"); //Missed the value1 + result.ShouldBe("ModelState.IsValid: false"); + } + } +}