// 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.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Internal; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { public class ViewDataDictionaryTest { [Fact] public void ConstructorWithOneParameterInitalizesMembers() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); // Act var viewData = new ViewDataDictionary(metadataProvider); // Assert Assert.NotNull(viewData.ModelState); Assert.NotNull(viewData.TemplateInfo); Assert.Null(viewData.Model); Assert.NotNull(viewData.ModelMetadata); Assert.Equal(0, viewData.Count); } [Fact] public void ConstructorInitalizesMembers() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var modelState = new ModelStateDictionary(); // Act var viewData = new ViewDataDictionary(metadataProvider, modelState); // Assert Assert.Same(modelState, viewData.ModelState); Assert.NotNull(viewData.TemplateInfo); Assert.Null(viewData.Model); Assert.NotNull(viewData.ModelMetadata); Assert.Equal(0, viewData.Count); } [Fact] public void Constructor_GetsNewModelMetadata() { // Arrange var metadataProvider = new Mock(MockBehavior.Strict); metadataProvider .Setup(m => m.GetMetadataForType(typeof(object))) .Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(object))) .Verifiable(); var modelState = new ModelStateDictionary(); // Act var viewData = new ViewDataDictionary(metadataProvider.Object, modelState); // Assert Assert.NotNull(viewData.ModelMetadata); metadataProvider.Verify(m => m.GetMetadataForType(typeof(object)), Times.Once()); } [Fact] public void SetModel_DoesNotGetNewModelMetadata_IfTypeCompatible() { // Arrange var metadataProvider = new Mock(MockBehavior.Strict); metadataProvider .Setup(m => m.GetMetadataForType(typeof(TestModel))) .Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(TestModel))) .Verifiable(); var viewData = new TestViewDataDictionary(metadataProvider.Object, typeof(TestModel)); var model = new TestModel(); // Act viewData.SetModelPublic(model); // Assert Assert.NotNull(viewData.ModelMetadata); metadataProvider.Verify(m => m.GetMetadataForType(typeof(TestModel)), Times.Once()); } [Fact] public void SetModel_GetsNewModelMetadata_IfSourceTypeIsObject() { // Arrange var metadataProvider = new Mock(MockBehavior.Strict); metadataProvider .Setup(m => m.GetMetadataForType(typeof(object))) .Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(object))) .Verifiable(); metadataProvider .Setup(m => m.GetMetadataForType(typeof(TestModel))) .Returns(new EmptyModelMetadataProvider().GetMetadataForType(typeof(TestModel))) .Verifiable(); var viewData = new TestViewDataDictionary(metadataProvider.Object); var model = new TestModel(); // Act viewData.SetModelPublic(model); // Assert Assert.NotNull(viewData.ModelMetadata); // For the constructor. metadataProvider.Verify(m => m.GetMetadataForType(typeof(object)), Times.Once()); // For SetModel(). metadataProvider.Verify(m => m.GetMetadataForType(typeof(TestModel)), Times.Once()); } public static TheoryData IncompatibleModelData { get { // Small "anything but TestModel" grab bag of instances and expected types. return new TheoryData { { true, typeof(bool) }, { 23, typeof(int) }, { 43.78, typeof(double) }, { "test string", typeof(string) }, { new List(), typeof(List) }, { new List(), typeof(List) }, { new List(), typeof(List) }, }; } } [Theory] [MemberData(nameof(IncompatibleModelData))] public void SetModel_Throws_IfModelIncompatibleWithDeclaredType(object model, Type expectedType) { // Arrange var viewData = new TestViewDataDictionary(new EmptyModelMetadataProvider(), typeof(TestModel)); // Act & Assert var exception = Assert.Throws(() => viewData.SetModelPublic(model)); Assert.Equal( $"The model item passed into the ViewDataDictionary is of type '{ model.GetType() }', but this " + $"ViewDataDictionary instance requires a model item of type '{ typeof(TestModel) }'.", exception.Message); } public static TheoryData EnumerableModelData { get { var model = new List() { new TestModel(), new TestModel() }; return new TheoryData { { model.Select(t => t) }, { model.Where(t => t != null) }, { model.SelectMany(t => t.ToString()) }, { model.Take(2) }, { model.TakeWhile(t => t != null) }, { model.Union(model) } }; } } [Theory] [MemberData(nameof(EnumerableModelData))] public void ModelSetter_DoesNotThrowOnEnumerableModel(object model) { // Arrange var vdd = new ViewDataDictionary(new EmptyModelMetadataProvider()); // Act vdd.Model = model; // Assert Assert.Same(model, vdd.Model); } [Fact] public void CopyConstructorInitalizesModelAndModelMetadataBasedOnSource() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var model = new TestModel(); var source = new ViewDataDictionary(metadataProvider) { ModelExplorer = metadataProvider.GetModelExplorerForType(typeof(TestModel), model), }; source["foo"] = "bar"; source.TemplateInfo.HtmlFieldPrefix = "prefix"; // Act var viewData = new ViewDataDictionary(source); // Assert Assert.NotNull(viewData.ModelState); Assert.NotNull(viewData.TemplateInfo); Assert.Equal("prefix", viewData.TemplateInfo.HtmlFieldPrefix); Assert.NotSame(source.TemplateInfo, viewData.TemplateInfo); Assert.Same(model, viewData.Model); Assert.NotNull(viewData.ModelMetadata); Assert.Equal(typeof(TestModel), viewData.ModelMetadata.ModelType); Assert.Same(source.ModelMetadata, viewData.ModelMetadata); Assert.Equal(source.Count, viewData.Count); Assert.Equal("bar", viewData["foo"]); Assert.IsType>(viewData.Data); } public static TheoryData CopyModelMetadataData { get { // Instances in this data set must have exactly the same type as the corresponding Type or be null. // Otherwise the copy constructor ignores the source ModelMetadata. return new TheoryData { { typeof(int), 23 }, { typeof(ulong?), 24ul }, { typeof(ushort?), null }, { typeof(string), "hello" }, { typeof(string), null }, { typeof(List), new List() }, { typeof(string[]), new string[0] }, { typeof(Dictionary), new Dictionary() }, }; } } [Theory] [MemberData(nameof(CopyModelMetadataData))] public void CopyConstructor_CopiesModelMetadata(Type type, object instance) { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var source = new ViewDataDictionary(metadataProvider) { Model = instance, }; // Act var viewData = new ViewDataDictionary(source); // Assert Assert.Same(source.ModelMetadata, viewData.ModelMetadata); } [Fact] public void CopyConstructor_CopiesModelMetadata_ForTypeObject() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var source = new ViewDataDictionary(metadataProvider); // Act var viewData = new ViewDataDictionary(source); // Assert Assert.Same(source.ModelMetadata, viewData.ModelMetadata); Assert.Equal(typeof(object), viewData.ModelMetadata.ModelType); } [Theory] [InlineData(typeof(int), "test string", typeof(string))] [InlineData(typeof(string), 23, typeof(int))] [InlineData(typeof(IEnumerable), new object[] { "1", "2", "3" }, typeof(object[]))] [InlineData(typeof(List), new object[] { 1, 2, 3 }, typeof(object[]))] public void ModelSetter_UpdatesModelMetadata_IfModelIncompatibleWithSourceMetadata( Type sourceType, object model, Type expectedType) { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var source = new ViewDataDictionary(metadataProvider) { ModelExplorer = metadataProvider.GetModelExplorerForType(sourceType, model: null), }; var sourceMetadata = source.ModelMetadata; var viewData = new ViewDataDictionary(source); // Act viewData.Model = model; // Assert Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer); Assert.NotSame(source.ModelMetadata, viewData.ModelMetadata); Assert.Equal(expectedType, viewData.ModelMetadata.ModelType); } [Theory] [InlineData(typeof(int), 23)] [InlineData(typeof(string), "test string")] [InlineData(typeof(IEnumerable), new string[] { "1", "2", "3" })] public void ModelSetter_PreservesSourceMetadata_IfModelCompatible(Type sourceType, object model) { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var source = new ViewDataDictionary(metadataProvider) { ModelExplorer = metadataProvider.GetModelExplorerForType(sourceType, model: null), }; var viewData = new ViewDataDictionary(source); // Act viewData.Model = model; // Assert Assert.NotSame(source.ModelExplorer, viewData.ModelExplorer); Assert.Same(source.ModelMetadata, viewData.ModelMetadata); } [Fact] public void ModelSetter_SameType_UpdatesModelExplorer() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var viewData = new ViewDataDictionary(metadataProvider) { Model = 3, }; var originalMetadata = viewData.ModelMetadata; var originalExplorer = viewData.ModelExplorer; // Act viewData.Model = 5; // Assert Assert.NotNull(viewData.ModelMetadata); Assert.NotNull(viewData.ModelExplorer); Assert.Equal(5, viewData.Model); Assert.Equal(5, viewData.ModelExplorer.Model); Assert.Same(originalMetadata, viewData.ModelMetadata); Assert.NotSame(originalExplorer, viewData.ModelExplorer); } [Theory] [InlineData(typeof(object))] [InlineData(typeof(string))] public void ModelSetter_DifferentType_UpdatesModelMetadata(Type originalMetadataType) { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForType(originalMetadataType); var explorer = new ModelExplorer(metadataProvider, metadata, model: null); var viewData = new TestViewDataDictionary(metadataProvider) { ModelExplorer = explorer, }; // Act viewData.Model = true; // Assert Assert.NotNull(viewData.ModelExplorer); Assert.NotNull(viewData.ModelMetadata); Assert.NotSame(explorer, viewData.ModelExplorer); Assert.Equal(typeof(bool), viewData.ModelMetadata.ModelType); var model = Assert.IsType(viewData.Model); Assert.True(model); } [Fact] public void ModelSetter_SetNullableNonNull_UpdatesModelExplorer() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(bool?)); var explorer = new ModelExplorer(metadataProvider, metadata, model: null); var viewData = new ViewDataDictionary(metadataProvider) { ModelExplorer = explorer, }; // Act viewData.Model = true; // Assert Assert.NotNull(viewData.ModelMetadata); Assert.NotNull(viewData.ModelExplorer); Assert.Same(metadata, viewData.ModelMetadata); Assert.NotSame(explorer, viewData.ModelExplorer); Assert.Equal(viewData.Model, viewData.ModelExplorer.Model); var model = Assert.IsType(viewData.Model); Assert.True(model); } [Fact] public void ModelSetter_SetNonNullableToNull_Throws() { // Arrange var viewData = new TestViewDataDictionary(new EmptyModelMetadataProvider(), typeof(int)); // Act & Assert var exception = Assert.Throws(() => viewData.SetModelPublic(value: null)); Assert.Equal( "The model item passed is null, but this ViewDataDictionary instance requires a non-null model item " + $"of type '{ typeof(int) }'.", exception.Message); } [Fact] public void ModelSetter_SameType_BoxedValueTypeUpdatesModelExplorer() { // Arrange var metadataProvider = new EmptyModelMetadataProvider(); var viewData = new ViewDataDictionary(metadataProvider) { Model = 3, }; var originalMetadata = viewData.ModelMetadata; var originalExplorer = viewData.ModelExplorer; // Act viewData.Model = 3; // This is the same value, but it's in a different box. // Assert Assert.NotNull(viewData.ModelMetadata); Assert.NotNull(viewData.ModelExplorer); Assert.Equal(3, viewData.Model); Assert.Equal(3, viewData.ModelExplorer.Model); Assert.Same(originalMetadata, viewData.ModelMetadata); Assert.NotSame(originalExplorer, viewData.ModelExplorer); } [Fact] public void ModelSetter_SameModel_NoChanges() { // Arrange var model = "Hello"; var metadataProvider = new EmptyModelMetadataProvider(); var viewData = new ViewDataDictionary(metadataProvider) { Model = model, }; var originalMetadata = viewData.ModelMetadata; var originalExplorer = viewData.ModelExplorer; // Act viewData.Model = model; // Assert Assert.NotNull(viewData.ModelMetadata); Assert.Equal("Hello", viewData.Model); Assert.Same(originalMetadata, viewData.ModelMetadata); Assert.Same(originalExplorer, viewData.ModelExplorer); } public static TheoryData Eval_EvaluatesExpressionsData { get { return new TheoryData { { new { Foo = "Bar" }, "Foo", "Bar" }, { new { Foo = new Dictionary { { "Bar", "Baz" } } }, "Foo.Bar", "Baz" }, { new { Foo = new { Bar = "Baz" } }, "Foo.Bar", "Baz" } }; } } [Theory] [MemberData(nameof(Eval_EvaluatesExpressionsData))] public void Eval_EvaluatesExpressions(object model, string expression, object expected) { // Arrange var viewData = GetViewDataDictionary(model); // Act var result = viewData.Eval(expression); // Assert Assert.Equal(expected, result); } [Fact] public void EvalReturnsNullIfExpressionDoesNotMatch() { // Arrange var model = new { Foo = new { Biz = "Baz" } }; var viewData = GetViewDataDictionary(model); // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Null(result); } [Fact] public void EvalEvaluatesDictionaryThenModel() { // Arrange var model = new { Foo = "NotBar" }; var viewData = GetViewDataDictionary(model); viewData.Add("Foo", "Bar"); // Act var result = viewData.Eval("Foo"); // Assert Assert.Equal("Bar", result); } [Fact] public void EvalReturnsValueJustAdded() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo", "Blah"); // Act var result = viewData.Eval("Foo"); // Assert Assert.Equal("Blah", result); } [Fact] public void EvalWithCompoundExpressionReturnsIndexedValue() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo.Bar", "Baz"); // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Equal("Baz", result); } [Fact] public void EvalWithCompoundExpressionReturnsPropertyOfAddedObject() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo", new { Bar = "Baz" }); // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Equal("Baz", result); } [Fact] public void EvalWithCompoundIndexExpressionReturnsEval() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo.Bar", new { Baz = "Quux" }); // Act var result = viewData.Eval("Foo.Bar.Baz"); // Assert Assert.Equal("Quux", result); } [Fact] public void EvalWithCompoundIndexAndCompoundExpressionReturnsValue() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo.Bar", new { Baz = new { Blah = "Quux" } }); // Act var result = viewData.Eval("Foo.Bar.Baz.Blah"); // Assert Assert.Equal("Quux", result); } // Make sure that dict["foo.bar"] gets chosen before dict["foo"]["bar"] [Fact] public void EvalChoosesValueInDictionaryOverOtherValue() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()) { { "Foo", new { Bar = "Not Baz" } }, { "Foo.Bar", "Baz" } }; // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Equal("Baz", result); } // Make sure that dict["foo.bar"]["baz"] gets chosen before dict["foo"]["bar"]["baz"] [Fact] public void EvalChoosesCompoundValueInDictionaryOverOtherValues() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()) { { "Foo", new { Bar = new { Baz = "Not Quux" } } }, { "Foo.Bar", new { Baz = "Quux" } } }; // Act var result = viewData.Eval("Foo.Bar.Baz"); // Assert Assert.Equal("Quux", result); } // Make sure that dict["foo.bar"]["baz"] gets chosen before dict["foo"]["bar.baz"] [Fact] public void EvalChoosesCompoundValueInDictionaryOverOtherValuesWithCompoundProperty() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()) { { "Foo", new Person() }, { "Foo.Bar", new { Baz = "Quux" } } }; // Act var result = viewData.Eval("Foo.Bar.Baz"); // Assert Assert.Equal("Quux", result); } [Theory] [InlineData(null)] [InlineData("")] public void Eval_ReturnsModel_IfExpressionIsNullOrEmpty(string expression) { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); var model = new object(); viewData.Model = model; // Act var result = viewData.Eval(expression); // Assert Assert.Same(model, result); } [Fact] public void EvalWithCompoundExpressionAndDictionarySubExpressionChoosesDictionaryValue() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo", new Dictionary { { "Bar", "Baz" } }); // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Equal("Baz", result); } [Fact] public void EvalWithDictionaryAndNoMatchReturnsNull() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData.Add("Foo", new Dictionary { { "NotBar", "Baz" } }); // Act var result = viewData.Eval("Foo.Bar"); // Assert Assert.Null(result); } [Fact] public void EvalWithNestedDictionariesEvalCorrectly() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); var value = new Dictionary { ["Bar"] = new Dictionary { { "Baz", "Quux" } } }; viewData.Add("Foo", value); // Act var result = viewData.Eval("Foo.Bar.Baz"); // Assert Assert.Equal("Quux", result); } [Fact] public void EvalFormatWithNullValueReturnsEmptyString() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); // Act var formattedValue = viewData.Eval("foo", "for{0}mat"); // Assert Assert.Empty(formattedValue); } [Fact] public void EvalFormatWithEmptyFormatReturnsViewData() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData["foo"] = "value"; // Act var formattedValue = viewData.Eval("foo", string.Empty); // Assert Assert.Equal("value", formattedValue); } [Fact] public void EvalFormatWithFormatReturnsFormattedViewData() { // Arrange var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider()); viewData["foo"] = "value"; // Act var formattedValue = viewData.Eval("foo", "for{0}mat"); // Assert Assert.Equal("forvaluemat", formattedValue); } [Fact] public void EvalPropertyNamedModel() { // Arrange var model = new TheQueryStringParam { Name = "The Name", Value = "The Value", Model = "The Model", }; var viewData = GetViewDataDictionary(model); viewData["Title"] = "Home Page"; viewData["Message"] = "Welcome to ASP.NET MVC!"; // Act var result = viewData.Eval("Model"); // Assert Assert.Equal("The Model", result); } [Fact] public void EvalSubPropertyNamedValueInModel() { // Arrange var model = new TheQueryStringParam { Name = "The Name", Value = "The Value", Model = "The Model", }; var viewData = GetViewDataDictionary(model); viewData["Title"] = "Home Page"; viewData["Message"] = "Welcome to ASP.NET MVC!"; // Act var result = viewData.Eval("Value"); // Assert Assert.Equal("The Value", result); } private static ViewDataDictionary GetViewDataDictionary(object model) { return new ViewDataDictionary(new EmptyModelMetadataProvider()) { Model = model }; } private class TestModel { } private class Person { public string Name { get; set; } } private class TestViewDataDictionary : ViewDataDictionary { public TestViewDataDictionary(IModelMetadataProvider metadataProvider) : base(metadataProvider) { } public TestViewDataDictionary(IModelMetadataProvider metadataProvider, Type declaredModelType) : base(metadataProvider, declaredModelType) { } public void SetModelPublic(object value) { SetModel(value); } } private class TheQueryStringParam { public string Name { get; set; } public string Value { get; set; } public string Model { get; set; } } } }