From e41e5066f991c8ca74f8d6d87d2f41590784b634 Mon Sep 17 00:00:00 2001 From: Kirthi Krishnamraju Date: Tue, 13 Jan 2015 16:30:06 -0800 Subject: [PATCH] Added support for TryValidateModel and its corresponding tests --- src/Microsoft.AspNet.Mvc.Core/Controller.cs | 53 +++++++++- .../ControllerTests.cs | 100 ++++++++++++++++++ .../TryValidateModelTest.cs | 73 +++++++++++++ .../Controllers/TryValidateModelController.cs | 64 +++++++++++ .../FormatterWebSite/Models/AdminValidator.cs | 22 ++++ .../FormatterWebSite/Models/Administrator.cs | 13 +++ 6 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs create mode 100644 test/WebSites/FormatterWebSite/Controllers/TryValidateModelController.cs create mode 100644 test/WebSites/FormatterWebSite/Models/AdminValidator.cs create mode 100644 test/WebSites/FormatterWebSite/Models/Administrator.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index e0fd035e68..72661425a0 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -1030,6 +1030,57 @@ namespace Microsoft.AspNet.Mvc predicate); } + /// + /// Validates the specified instance. + /// + /// The model to validate. + /// true if the is valid; false otherwise. + [NonAction] + public virtual bool TryValidateModel([NotNull] object model) + { + return TryValidateModel(model, prefix: null); + } + + /// + /// Validates the specified instance. + /// + /// The model to validate. + /// The key to use when looking up information in . + /// + /// true if the is valid;false otherwise. + [NonAction] + public virtual bool TryValidateModel([NotNull] object model, string prefix) + { + if (BindingContext == null) + { + var message = Resources.FormatPropertyOfTypeCannotBeNull( + nameof(BindingContext), + typeof(Controller).FullName); + throw new InvalidOperationException(message); + } + + var modelMetadata = MetadataProvider.GetMetadataForType( + modelAccessor: () => model, + modelType: model.GetType()); + + var validationContext = new ModelValidationContext( + MetadataProvider, + BindingContext.ValidatorProvider, + ModelState, + modelMetadata, + containerMetadata: null); + + var modelName = prefix ?? string.Empty; + + var validationNode = new ModelValidationNode(modelMetadata, modelName) + { + ValidateAllProperties = true + }; + validationNode.Validate(validationContext); + + return ModelState.IsValid; + } + public void Dispose() { Dispose(disposing: true); @@ -1040,4 +1091,4 @@ namespace Microsoft.AspNet.Mvc { } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs index 0fbdee98f4..23e0ac6f1e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs @@ -1237,6 +1237,101 @@ namespace Microsoft.AspNet.Mvc.Test Assert.True(controller.DisposeCalled); } + [Fact] + public void TryValidateModelWithValidModel_ReturnsTrue() + { + // Arrange + var binder = new Mock(); + var controller = GetController(binder.Object, provider: null); + controller.BindingContext.ValidatorProvider = Mock.Of(); + var model = new TryValidateModelModel(); + + // Act + var result = controller.TryValidateModel(model); + + // Assert + Assert.True(result); + Assert.True(controller.ModelState.IsValid); + } + + [Fact] + public void TryValidateModelWithInvalidModelWithPrefix_ReturnsFalse() + { + // Arrange + var model = new TryValidateModelModel(); + var validationResult = new [] + { + new ModelValidationResult(string.Empty, "Out of range!") + }; + + var validator1 = new Mock(); + + validator1.Setup(v => v.Validate(It.IsAny())) + .Returns(validationResult); + + var provider = new Mock(); + provider.Setup(v => v.GetValidators(It.IsAny())) + .Returns(new[] { validator1.Object }); + + var binder = new Mock(); + var controller = GetController(binder.Object, provider: null); + controller.BindingContext.ValidatorProvider = provider.Object; + + // Act + var result = controller.TryValidateModel(model, "Prefix"); + + // Assert + Assert.False(result); + Assert.Equal(1, controller.ModelState.Count); + var error = Assert.Single(controller.ModelState["Prefix.IntegerProperty"].Errors); + Assert.Equal("Out of range!", error.ErrorMessage); + } + + [Fact] + public void TryValidateModelWithInvalidModelNoPrefix_ReturnsFalse() + { + // Arrange + var model = new TryValidateModelModel(); + var validationResult = new[] + { + new ModelValidationResult(string.Empty, "Out of range!") + }; + + var validator1 = new Mock(); + + validator1.Setup(v => v.Validate(It.IsAny())) + .Returns(validationResult); + + var provider = new Mock(); + provider.Setup(v => v.GetValidators(It.IsAny())) + .Returns(new[] { validator1.Object }); + + var binder = new Mock(); + var controller = GetController(binder.Object, provider: null); + controller.BindingContext.ValidatorProvider = provider.Object; + + // Act + var result = controller.TryValidateModel(model); + + // Assert + Assert.False(result); + Assert.Equal(1, controller.ModelState.Count); + var error = Assert.Single(controller.ModelState["IntegerProperty"].Errors); + Assert.Equal("Out of range!", error.ErrorMessage); + } + + [Fact] + public void TryValidateModelEmptyBindingContextThrowsException() + { + // Arrange + var controller = new Controller(); + var model = new TryValidateModelModel(); + + // Act & Assert + var exception = Assert.Throws(() => controller.TryValidateModel(model)); + Assert.Equal("The 'BindingContext' property of 'Microsoft.AspNet.Mvc.Controller' must not be null.", exception.Message); + } + private static Controller GetController(IModelBinder binder, IValueProvider provider) { var metadataProvider = new DataAnnotationsModelMetadataProvider(); @@ -1287,6 +1382,11 @@ namespace Microsoft.AspNet.Mvc.Test public int Zip { get; set; } } + private class TryValidateModelModel + { + public int IntegerProperty { get; set; } + } + private class DisposableController : Controller { public bool DisposeCalled { get; private set; } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs new file mode 100644 index 0000000000..23d9364d79 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TryValidateModelTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class TryValidateModelTest + { + private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(FormatterWebSite)); + private readonly Action _app = new FormatterWebSite.Startup().Configure; + + [Fact] + public async Task TryValidateModel_SimpleModelInvalidProperties() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/TryValidateModel/GetInvalidUser"); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var json = JsonConvert.DeserializeObject>( + await response.Content.ReadAsStringAsync()); + Assert.Equal("The field Id must be between 1 and 2000.", json["Id"][0]); + Assert.Equal( + "The field Name must be a string or array type with a minimum length of '5'.", json["Name"][0]); + } + + [Fact] + public async Task TryValidateModel_DerivedModelInvalidType() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/TryValidateModel/GetInvalidAdminWithPrefix"); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var json = JsonConvert.DeserializeObject>( + await response.Content.ReadAsStringAsync()); + Assert.Equal("AdminAccessCode property does not have the right value", json["admin"][0]); + } + + [Fact] + public async Task TryValidateModel_ValidDerivedModel() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/TryValidateModel/GetValidAdminWithPrefix"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Admin user created successfully", await response.Content.ReadAsStringAsync()); + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Controllers/TryValidateModelController.cs b/test/WebSites/FormatterWebSite/Controllers/TryValidateModelController.cs new file mode 100644 index 0000000000..d041232b94 --- /dev/null +++ b/test/WebSites/FormatterWebSite/Controllers/TryValidateModelController.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace FormatterWebSite +{ + public class TryValidateModelController : Controller + { + [HttpGet] + public IActionResult GetInvalidUser() + { + var user = new User + { + Id = 0, + Name = "x" + }; + + // If ModelState.InValid is false return BadRequestOjectResult; else return empty string. + if (!TryValidateModel(user)) + { + return new BadRequestObjectResult(ModelState); + } + + return Content(string.Empty); + } + + [HttpGet] + public IActionResult GetInvalidAdminWithPrefix() + { + var admin = new Administrator() + { + Id = 1, + Name = "John Doe", + Designation = "Administrator", + AdminAccessCode = 0 + }; + if (!TryValidateModel(admin,"admin")) + { + return new BadRequestObjectResult(ModelState); + } + + return Content(string.Empty); + } + + [HttpGet] + public IActionResult GetValidAdminWithPrefix() + { + var admin = new Administrator() + { + Id = 1, + Name = "John Doe", + Designation = "Administrator", + AdminAccessCode = 1 + }; + if (!TryValidateModel(admin, "admin")) + { + return new BadRequestObjectResult(ModelState); + } + + return Content("Admin user created successfully"); + } + } +} diff --git a/test/WebSites/FormatterWebSite/Models/AdminValidator.cs b/test/WebSites/FormatterWebSite/Models/AdminValidator.cs new file mode 100644 index 0000000000..03ebcf4b4a --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/AdminValidator.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace FormatterWebSite +{ + public class AdminValidator : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var admin = (Administrator)value; + + if (admin.AdminAccessCode != 1) + { + return new ValidationResult ("AdminAccessCode property does not have the right value"); + } + + return null; + } + } +} \ No newline at end of file diff --git a/test/WebSites/FormatterWebSite/Models/Administrator.cs b/test/WebSites/FormatterWebSite/Models/Administrator.cs new file mode 100644 index 0000000000..ce82dd9ded --- /dev/null +++ b/test/WebSites/FormatterWebSite/Models/Administrator.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace FormatterWebSite +{ + [AdminValidator] + public class Administrator : User + { + public int AdminAccessCode { get; set; } + } +} \ No newline at end of file