// 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 System.Linq.Expressions; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public class ExpressionHelperTest { private readonly ExpressionTextCache _expressionTextCache = new ExpressionTextCache(); public static TheoryData ExpressionAndTexts { get { var i = 3; var value = "Test"; var key = "TestModel"; var myModels = new List(); var models = new List(); var modelTest = new TestModel(); var modelType = typeof(TestModel); var data = new TheoryData { { (Expression>)(model => model.SelectedCategory), "SelectedCategory" }, { (Expression>)(model => model.SelectedCategory.CategoryName), "SelectedCategory.CategoryName" }, { (Expression>)(testModel => testModel.SelectedCategory.CategoryId), "SelectedCategory.CategoryId" }, { (Expression>)(testModel => testModel.selectedcategory.CategoryId), "selectedcategory.CategoryId" }, { (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory), "SelectedCategory.CategoryName.MainCategory" }, { (Expression>)(model => model), string.Empty }, { (Expression>)(model => value), "value" }, { (Expression>)(model => models[0].SelectedCategory.CategoryId), "models[0].SelectedCategory.CategoryId" }, { (Expression>)(model => modelTest.Name), "modelTest.Name" }, { (Expression>)(model => modelType), "modelType" }, { (Expression, Category>>)(model => model[2].SelectedCategory), "[2].SelectedCategory" }, { (Expression, Category>>)(model => model[i].SelectedCategory), "[3].SelectedCategory" }, { (Expression, Category>>)(model => model[i].selectedcategory), "[3].selectedcategory" }, { (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), "[TestModel].SelectedCategory.CategoryName.MainCategory" }, { (Expression>)(model => model.PreferredCategories[i].CategoryId), "PreferredCategories[3].CategoryId" }, { (Expression, Category>>)(model => myModels[i].SelectedCategory), "myModels[3].SelectedCategory" }, { (Expression, int>>)(model => model[2].PreferredCategories[i].CategoryId), "[2].PreferredCategories[3].CategoryId" }, { (Expression, int>>)(model => model[2].preferredcategories[i].CategoryId), "[2].preferredcategories[3].CategoryId" }, { (Expression, string>>)(model => model.FirstOrDefault().Name), "Name" }, { (Expression, string>>)(model => model.FirstOrDefault().name), "name" }, { (Expression, string>>)(model => model.FirstOrDefault().Model), "Model" }, { (Expression, int>>)(model => model.FirstOrDefault().SelectedCategory.CategoryId), "SelectedCategory.CategoryId" }, { (Expression, string>>)(model => model.FirstOrDefault().SelectedCategory.CategoryName.MainCategory), "SelectedCategory.CategoryName.MainCategory" }, { (Expression, int>>)(model => model.FirstOrDefault().PreferredCategories.Count), "PreferredCategories.Count" }, { (Expression, int>>)(model => model.FirstOrDefault().PreferredCategories.FirstOrDefault().CategoryId), "CategoryId" }, // Constants are not supported. { // Namespace never appears in expression name. "Model" there doesn't matter. (Expression>)(m => Microsoft.AspNetCore.Mvc.ViewFeatures.Model.Constants.WoodstockYear), string.Empty }, { // Class name never appears in expression name. "Model" there doesn't matter. (Expression>)(m => Model.Constants.WoodstockYear), string.Empty }, // ExpressionHelper treats static properties like other member accesses. Similarly to // RazorPage.Model, name "Model" is ignored at LHS of these expressions. This is a rare case because // static properties are the only leftmost member accesses that can reach beyond the current class. { (Expression>)(m => Model.Constants.Model.Name), "Name" }, { (Expression>)(m => AStaticClass.Model), string.Empty }, { (Expression>)(m => AStaticClass.Test), "Test" }, { (Expression>)(m => AnotherStaticClass.Model.Name), "Name" }, { (Expression>)(m => AnotherStaticClass.Test.Name), "Test.Name" }, }; { // Nearly impossible in a .cshtml file because model is a keyword. var model = "Some string"; data.Add((Expression>)(m => model), string.Empty); } { // Model property in RazorPage is "special" (in a good way). var Model = new TestModel(); data.Add((Expression>)(m => Model), string.Empty); data.Add((Expression>)(model => Model), string.Empty); data.Add((Expression>)(m => Model.SelectedCategory), "SelectedCategory"); } return data; } } public static TheoryData CachedExpressions { get { var key = "TestModel"; var myModel = new TestModel(); return new TheoryData { (Expression>)(model => model.SelectedCategory), (Expression>)(model => model.SelectedCategory.CategoryName), (Expression>)(testModel => testModel.SelectedCategory.CategoryId), (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory), (Expression>)(testModel => key), (Expression>)(m => m), (Expression>)(m => myModel.SelectedCategory), }; } } public static TheoryData IndexerExpressions { get { var i = 3; var key = "TestModel"; var myModels = new List(); return new TheoryData { (Expression, Category>>)(model => model[2].SelectedCategory), (Expression, Category>>)(model => myModels[i].SelectedCategory), (Expression, CategoryName>>)(testModel => testModel[i].SelectedCategory.CategoryName), (Expression>)(model => model.PreferredCategories[i].CategoryId), (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), }; } } public static TheoryData UnsupportedExpressions { get { var i = 2; var j = 3; return new TheoryData { // Indexers that have multiple arguments. (Expression>)(model => model[23][3].Name), (Expression>)(model => model[i][3].Name), (Expression>)(model => model[23][j].Name), (Expression>)(model => model[i][j].Name), // Calls that aren't indexers. (Expression, string>>)(model => model.FirstOrDefault().Name), (Expression, string>>)(model => model.FirstOrDefault().SelectedCategory.CategoryName.MainCategory), (Expression, int>>)(model => model.FirstOrDefault().PreferredCategories.FirstOrDefault().CategoryId), }; } } public static TheoryData EquivalentExpressions { get { var value = "Test"; var Model = "Test"; return new TheoryData { { (Expression>)(model => model.SelectedCategory), (Expression>)(model => model.SelectedCategory) }, { (Expression>)(model => model.SelectedCategory.CategoryName), (Expression>)(model => model.SelectedCategory.CategoryName) }, { (Expression>)(testModel => testModel.SelectedCategory.CategoryId), (Expression>)(testModel => testModel.SelectedCategory.CategoryId) }, { (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory), (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory) }, { (Expression>)(model => model), (Expression>)(m => m) }, { (Expression>)(model => value), (Expression>)(m => value) }, { // These two expressions are not actually equivalent. However ExpressionHelper returns // string.Empty for these two expressions and hence they are considered as equivalent by the // cache. (Expression>)(m => Model), (Expression>)(m => m) }, }; } } public static TheoryData NonEquivalentExpressions { get { var value = "test"; var key = "TestModel"; var Model = "Test"; var myModel = new TestModel(); return new TheoryData { { (Expression>)(model => model.SelectedCategory), (Expression>)(model => model.SelectedCategory.CategoryName) }, { (Expression>)(model => model.SelectedCategory.CategoryName), (Expression>)(model => model.selectedcategory.CategoryName) }, { (Expression>)(model => model.Model), (Expression>)(model => model.Name) }, { (Expression>)(model => model.Model), (Expression>)(model => model.model) }, { (Expression>)(model => model.Name), (Expression>)(model => model.name) }, { (Expression>)(model => model.SelectedCategory.CategoryName), (Expression>)(model => value) }, { (Expression>)(testModel => testModel.SelectedCategory.CategoryName.MainCategory), (Expression>)(testModel => value) }, { (Expression, Category>>)(model => model[2].SelectedCategory), (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory) }, { (Expression, Category>>)(model => model[2].SelectedCategory), (Expression, Category>>)(model => model[2].selectedcategory) }, { (Expression>)(testModel => testModel.SelectedCategory.CategoryId), (Expression>)(model => model.SelectedCategory) }, { (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), (Expression>)(model => model.SelectedCategory) }, { (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), (Expression, string>>)(model => model[key].selectedcategory.CategoryName.MainCategory) }, { (Expression>)(m => Model), (Expression>)(m => m.Model) }, { (Expression>)(m => m), (Expression>)(m => m.Model) }, { (Expression>)(m => myModel.Name), (Expression>)(m => m.Name) }, { (Expression>)(m => key), (Expression>)(m => value) }, }; } } [Theory] [MemberData(nameof(ExpressionAndTexts))] public void GetExpressionText_ReturnsExpectedExpressionText(LambdaExpression expression, string expressionText) { // Act var text = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); // Assert Assert.Equal(expressionText, text); } [Theory] [MemberData(nameof(CachedExpressions))] public void GetExpressionText_CachesExpression(LambdaExpression expression) { // Act - 1 var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); // Act - 2 var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); // Assert Assert.Same(text1, text2); // cached } [Theory] [MemberData(nameof(IndexerExpressions))] [MemberData(nameof(UnsupportedExpressions))] public void GetExpressionText_DoesNotCacheIndexerOrUnspportedExpression(LambdaExpression expression) { // Act - 1 var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); // Act - 2 var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); // Assert Assert.Equal(text1, text2, StringComparer.Ordinal); Assert.NotSame(text1, text2); // not cached } [Theory] [MemberData(nameof(EquivalentExpressions))] public void GetExpressionText_CacheEquivalentExpressions(LambdaExpression expression1, LambdaExpression expression2) { // Act - 1 var text1 = ExpressionHelper.GetExpressionText(expression1, _expressionTextCache); // Act - 2 var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache); // Assert Assert.Same(text1, text2); // cached } [Theory] [MemberData(nameof(NonEquivalentExpressions))] public void GetExpressionText_CheckNonEquivalentExpressions(LambdaExpression expression1, LambdaExpression expression2) { // Act - 1 var text1 = ExpressionHelper.GetExpressionText(expression1, _expressionTextCache); // Act - 2 var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache); // Assert Assert.NotEqual(text1, text2, StringComparer.Ordinal); Assert.NotSame(text1, text2); } private class TestModel { public string Name { get; set; } public string Model { get; set; } public Category SelectedCategory { get; set; } public IList PreferredCategories { get; set; } } private class LowerModel { public string name { get; set; } public string model { get; set; } public Category selectedcategory { get; set; } public IList preferredcategories { get; set; } } private class Category { public int CategoryId { get; set; } public CategoryName CategoryName { get; set; } } private class CategoryName { public string MainCategory { get; set; } public string SubCategory { get; set; } } private static class AStaticClass { public static string Model { get; set; } public static string Test { get; set; } } private static class AnotherStaticClass { public static Model.Model Model { get; set; } public static Model.Model Test { get; set; } } } }