From b28debf442f9041bddd78a629d09b5741ab09ec2 Mon Sep 17 00:00:00 2001 From: Muchiachio Date: Tue, 22 Sep 2015 20:23:10 +0300 Subject: [PATCH] Generic ModelStateDictionary add and remove extensions - Added generic add model error and remove extensions for ModelStateDictionary, to avoid using hard coded strings then specifying model state dictionary keys. - Added generic removal all extension for ModelStateDictionary, to support removing all the model state keys for given expression. aspnet/Mvc#3164 --- .../ModelStateDictionaryExtensions.cs | 114 ++++++ .../ModelStateDictionaryExtensionsTest.cs | 332 ++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs new file mode 100644 index 0000000000..1dfce56095 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. 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.Linq; +using System.Linq.Expressions; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ViewFeatures; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Extensions methods for . + /// + public static class ModelStateDictionaryExtensions + { + /// + /// Adds the specified to the instance + /// that is associated with the specified . + /// + /// The type of the model. + /// The instance this method extends. + /// An expression to be evaluated against an item in the current model. + /// The error message to add. + public static void AddModelError(this ModelStateDictionary modelState, Expression> expression, string errorMessage) + { + modelState.AddModelError(GetExpressionText(expression), errorMessage); + } + + /// + /// Adds the specified to the instance + /// that is associated with the specified . + /// + /// The type of the model. + /// The instance this method extends. + /// An expression to be evaluated against an item in the current model. + /// The to add. + public static void AddModelError(this ModelStateDictionary modelState, Expression> expression, Exception exception) + { + modelState.AddModelError(GetExpressionText(expression), exception); + } + + /// + /// Removes the specified from the . + /// + /// The type of the model. + /// The instance this method extends. + /// An expression to be evaluated against an item in the current model. + /// + /// true if the element is successfully removed; otherwise, false. + /// This method also returns false if was not found in the model-state dictionary. + /// + public static bool Remove(this ModelStateDictionary modelState, Expression> expression) + { + return modelState.Remove(GetExpressionText(expression)); + } + + /// + /// Removes all the entries for the specified from the . + /// + /// The type of the model. + /// The instance this method extends. + /// An expression to be evaluated against an item in the current model. + public static void RemoveAll(this ModelStateDictionary modelState, Expression> expression) + { + string modelKey = GetExpressionText(expression); + if (string.IsNullOrEmpty(modelKey)) + { + var modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(TModel)); + + foreach (var property in modelMetadata.Properties) + { + var childKey = property.BinderModelName ?? property.PropertyName; + var entries = modelState.FindKeysWithPrefix(childKey).ToArray(); + foreach (var entry in entries) + { + modelState.Remove(entry.Key); + } + } + } + else + { + var entries = modelState.FindKeysWithPrefix(modelKey).ToArray(); + foreach (var entry in entries) + { + modelState.Remove(entry.Key); + } + } + } + + private static string GetExpressionText(LambdaExpression expression) + { + // We check if expression is wrapped with conversion to object expression + // and unwrap it if necessary, because Expression> + // automatically creates a convert to object expression for expresions + // returning value types + var unaryExpression = expression.Body as UnaryExpression; + + if (IsConversionToObject(unaryExpression)) + { + return ExpressionHelper.GetExpressionText(Expression.Lambda(unaryExpression.Operand, expression.Parameters[0])); + } + + return ExpressionHelper.GetExpressionText(expression); + } + + private static bool IsConversionToObject(UnaryExpression expression) + { + return expression?.NodeType == ExpressionType.Convert && + expression.Operand?.NodeType == ExpressionType.MemberAccess && + expression.Type == typeof(object); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs new file mode 100644 index 0000000000..b1e49feeb0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ModelStateDictionaryExtensionsTest.cs @@ -0,0 +1,332 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class ModelStateDictionaryExtensionsTest + { + [Fact] + public void AddModelError_ForSingleExpression_AddsExpectedMessage() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Text, "Message"); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Text", modelState.Key); + Assert.Equal("Message", modelError.ErrorMessage); + } + + [Fact] + public void AddModelError_ForRelationExpression_AddsExpectedMessage() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Child.Text, "Message"); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Child.Text", modelState.Key); + Assert.Equal("Message", modelError.ErrorMessage); + } + + [Fact] + public void AddModelError_ForImplicitlyCastedToObjectExpression_AddsExpectedMessage() + { + // Arrange + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Child.Value, "Message"); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Child.Value", modelState.Key); + Assert.Equal("Message", modelError.ErrorMessage); + } + + [Fact] + public void AddModelError_ForNotModelsExpression_AddsExpectedMessage() + { + // Arrange + var variable = "Test"; + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => variable, "Message"); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("variable", modelState.Key); + Assert.Equal("Message", modelError.ErrorMessage); + } + + [Fact] + public void AddModelError_ForSingleExpression_AddsExpectedException() + { + // Arrange + var exception = new Exception(); + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Text, exception); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Text", modelState.Key); + Assert.Same(exception, modelError.Exception); + } + + [Fact] + public void AddModelError_ForRelationExpression_AddsExpectedException() + { + // Arrange + var exception = new Exception(); + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Child.Text, exception); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Child.Text", modelState.Key); + Assert.Same(exception, modelError.Exception); + } + + [Fact] + public void AddModelError_ForImplicitlyCastedToObjectExpression_AddsExpectedException() + { + // Arrange + var exception = new Exception(); + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => model.Child.Value, exception); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("Child.Value", modelState.Key); + Assert.Same(exception, modelError.Exception); + } + + [Fact] + public void AddModelError_ForNotModelsExpression_AddsExpectedException() + { + // Arrange + var variable = "Test"; + var exception = new Exception(); + var dictionary = new ModelStateDictionary(); + + // Act + dictionary.AddModelError(model => variable, exception); + + // Assert + var modelState = Assert.Single(dictionary); + var modelError = Assert.Single(modelState.Value.Errors); + + Assert.Equal("variable", modelState.Key); + Assert.Same(exception, modelError.Exception); + } + + [Fact] + public void Remove_ForSingleExpression_RemovesModelStateKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.Add("Text", new ModelState()); + + // Act + dictionary.Remove(model => model.Text); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void Remove_ForRelationExpression_RemovesModelStateKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.Add("Child.Text", new ModelState()); + + // Act + dictionary.Remove(model => model.Child.Text); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void Remove_ForImplicitlyCastedToObjectExpression_RemovesModelStateKey() + { + // Arrange + var dictionary = new ModelStateDictionary(); + dictionary.Add("Child.Value", new ModelState()); + + // Act + dictionary.Remove(model => model.Child.Value); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void Remove_ForNotModelsExpression_RemovesModelStateKey() + { + // Arrange + var variable = "Test"; + var dictionary = new ModelStateDictionary(); + dictionary.Add("variable", new ModelState()); + + // Act + dictionary.Remove(model => variable); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void RemoveAll_ForSingleExpression_RemovesModelStateKeys() + { + // Arrange + var state = new ModelState(); + var dictionary = new ModelStateDictionary(); + + dictionary.Add("Key", state); + dictionary.Add("Text", new ModelState()); + dictionary.Add("Text.Length", new ModelState()); + + // Act + dictionary.RemoveAll(model => model.Text); + + // Assert + var modelState = Assert.Single(dictionary); + + Assert.Equal("Key", modelState.Key); + Assert.Same(state, modelState.Value); + } + + [Fact] + public void RemoveAll_ForRelationExpression_RemovesModelStateKeys() + { + // Arrange + var state = new ModelState(); + var dictionary = new ModelStateDictionary(); + + dictionary.Add("Key", state); + dictionary.Add("Child", new ModelState()); + dictionary.Add("Child.Text", new ModelState()); + + // Act + dictionary.RemoveAll(model => model.Child); + + // Assert + var modelState = Assert.Single(dictionary); + + Assert.Equal("Key", modelState.Key); + Assert.Same(state, modelState.Value); + } + + [Fact] + public void RemoveAll_ForImplicitlyCastedToObjectExpression_RemovesModelStateKeys() + { + // Arrange + var state = new ModelState(); + var dictionary = new ModelStateDictionary(); + + dictionary.Add("Child", state); + dictionary.Add("Child.Value", new ModelState()); + + // Act + dictionary.RemoveAll(model => model.Child.Value); + + // Assert + var modelState = Assert.Single(dictionary); + + Assert.Equal("Child", modelState.Key); + Assert.Same(state, modelState.Value); + } + + [Fact] + public void RemoveAll_ForNotModelsExpression_RemovesModelStateKeys() + { + // Arrange + var variable = "Test"; + var state = new ModelState(); + var dictionary = new ModelStateDictionary(); + + dictionary.Add("Key", state); + dictionary.Add("variable", new ModelState()); + dictionary.Add("variable.Text", new ModelState()); + dictionary.Add("variable.Value", new ModelState()); + + // Act + dictionary.RemoveAll(model => variable); + + // Assert + var modelState = Assert.Single(dictionary); + + Assert.Equal("Key", modelState.Key); + Assert.Same(state, modelState.Value); + } + + [Fact] + public void RemoveAll_ForModelExpression_RemovesModelPropertyKeys() + { + // Arrange + var state = new ModelState(); + var dictionary = new ModelStateDictionary(); + + dictionary.Add("Key", state); + dictionary.Add("Text", new ModelState()); + dictionary.Add("Child", new ModelState()); + dictionary.Add("Child.Text", new ModelState()); + dictionary.Add("Child.NoValue", new ModelState()); + + // Act + dictionary.RemoveAll(model => model); + + // Assert + var modelState = Assert.Single(dictionary); + + Assert.Equal("Key", modelState.Key); + Assert.Same(state, modelState.Value); + } + + private class TestModel + { + public string Text { get; set; } + + public ChildModel Child { get; set; } + } + + private class ChildModel + { + public int Value { get; set; } + public string Text { get; set; } + } + } +}