|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Linq.Expressions;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
public class ExpressionHelperTest
{
private readonly ConcurrentDictionary<LambdaExpression, string> _expressionTextCache = new ConcurrentDictionary<LambdaExpression, string>(LambdaExpressionComparer.Instance);
public static TheoryData<LambdaExpression, string> ExpressionAndTexts
{
get
{
var i = 3;
var value = "Test";
var key = "TestModel";
var myModels = new List<TestModel>();
var models = new List<TestModel>();
var modelTest = new TestModel();
var modelType = typeof(TestModel);
var data = new TheoryData<LambdaExpression, string>
{
{
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory),
"SelectedCategory"
},
{
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName),
"SelectedCategory.CategoryName"
},
{
(Expression<Func<TestModel, int>>)(testModel => testModel.SelectedCategory.CategoryId),
"SelectedCategory.CategoryId"
},
{
(Expression<Func<LowerModel, int>>)(testModel => testModel.selectedcategory.CategoryId),
"selectedcategory.CategoryId"
},
{
(Expression<Func<TestModel, string>>)(model => model.SelectedCategory.CategoryName.MainCategory),
"SelectedCategory.CategoryName.MainCategory"
},
{
(Expression<Func<TestModel, TestModel>>)(model => model),
string.Empty
},
{
(Expression<Func<TestModel, string>>)(model => value),
"value"
},
{
(Expression<Func<TestModel, int>>)(model => models[0].SelectedCategory.CategoryId),
"models[0].SelectedCategory.CategoryId"
},
{
(Expression<Func<TestModel, string>>)(model => modelTest.Name),
"modelTest.Name"
},
{
(Expression<Func<TestModel, Type>>)(model => modelType),
"modelType"
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
"[2].SelectedCategory"
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[i].SelectedCategory),
"[3].SelectedCategory"
},
{
(Expression<Func<IList<LowerModel>, Category>>)(model => model[i].selectedcategory),
"[3].selectedcategory"
},
{
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
"[TestModel].SelectedCategory.CategoryName.MainCategory"
},
{
(Expression<Func<TestModel, int>>)(model => model.PreferredCategories[i].CategoryId),
"PreferredCategories[3].CategoryId"
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => myModels[i].SelectedCategory),
"myModels[3].SelectedCategory"
},
{
(Expression<Func<IList<TestModel>, int>>)(model => model[2].PreferredCategories[i].CategoryId),
"[2].PreferredCategories[3].CategoryId"
},
{
(Expression<Func<IList<LowerModel>, int>>)(model => model[2].preferredcategories[i].CategoryId),
"[2].preferredcategories[3].CategoryId"
},
{
(Expression<Func<IList<TestModel>, string>>)(model => model.FirstOrDefault().Name),
"Name"
},
{
(Expression<Func<IList<LowerModel>, string>>)(model => model.FirstOrDefault().name),
"name"
},
{
(Expression<Func<IList<TestModel>, string>>)(model => model.FirstOrDefault().Model),
"Model"
},
{
(Expression<Func<IList<TestModel>, int>>)(model => model.FirstOrDefault().SelectedCategory.CategoryId),
"SelectedCategory.CategoryId"
},
{
(Expression<Func<IList<TestModel>, string>>)(model => model.FirstOrDefault().SelectedCategory.CategoryName.MainCategory),
"SelectedCategory.CategoryName.MainCategory"
},
{
(Expression<Func<IList<TestModel>, int>>)(model => model.FirstOrDefault().PreferredCategories.Count),
"PreferredCategories.Count"
},
{
(Expression<Func<IList<TestModel>, int>>)(model => model.FirstOrDefault().PreferredCategories.FirstOrDefault().CategoryId),
"CategoryId"
},
// Constants are not supported.
{
// Namespace never appears in expression name. "Model" there doesn't matter.
(Expression<Func<TestModel, int>>)(m => Microsoft.AspNetCore.Mvc.ViewFeatures.Model.Constants.WoodstockYear),
string.Empty
},
{
// Class name never appears in expression name. "Model" there doesn't matter.
(Expression<Func<TestModel, int>>)(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<Func<TestModel, string>>)(m => Model.Constants.Model.Name),
"Name"
},
{
(Expression<Func<TestModel, string>>)(m => AStaticClass.Model),
string.Empty
},
{
(Expression<Func<TestModel, string>>)(m => AStaticClass.Test),
"Test"
},
{
(Expression<Func<TestModel, string>>)(m => AnotherStaticClass.Model.Name),
"Name"
},
{
(Expression<Func<TestModel, string>>)(m => AnotherStaticClass.Test.Name),
"Test.Name"
},
};
{
// Nearly impossible in a .cshtml file because model is a keyword.
var model = "Some string";
data.Add((Expression<Func<TestModel, string>>)(m => model), string.Empty);
}
{
// Model property in RazorPage is "special" (in a good way).
var Model = new TestModel();
data.Add((Expression<Func<TestModel, TestModel>>)(m => Model), string.Empty);
data.Add((Expression<Func<TestModel, TestModel>>)(model => Model), string.Empty);
data.Add((Expression<Func<TestModel, Category>>)(m => Model.SelectedCategory), "SelectedCategory");
}
return data;
}
}
public static TheoryData<LambdaExpression> CachedExpressions
{
get
{
var key = "TestModel";
var myModel = new TestModel();
return new TheoryData<LambdaExpression>
{
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory),
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName),
(Expression<Func<TestModel, int>>)(testModel => testModel.SelectedCategory.CategoryId),
(Expression<Func<TestModel, string>>)(model => model.SelectedCategory.CategoryName.MainCategory),
(Expression<Func<TestModel, string>>)(testModel => key),
(Expression<Func<TestModel, TestModel>>)(m => m),
(Expression<Func<TestModel, Category>>)(m => myModel.SelectedCategory),
};
}
}
public static TheoryData<LambdaExpression> IndexerExpressions
{
get
{
var i = 3;
var key = "TestModel";
var myModels = new List<TestModel>();
return new TheoryData<LambdaExpression>
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
(Expression<Func<IList<TestModel>, Category>>)(model => myModels[i].SelectedCategory),
(Expression<Func<IList<TestModel>, CategoryName>>)(testModel => testModel[i].SelectedCategory.CategoryName),
(Expression<Func<TestModel, int>>)(model => model.PreferredCategories[i].CategoryId),
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
};
}
}
public static TheoryData<LambdaExpression> UnsupportedExpressions
{
get
{
var i = 2;
var j = 3;
return new TheoryData<LambdaExpression>
{
// Indexers that have multiple arguments.
(Expression<Func<TestModel[][], string>>)(model => model[23][3].Name),
(Expression<Func<TestModel[][], string>>)(model => model[i][3].Name),
(Expression<Func<TestModel[][], string>>)(model => model[23][j].Name),
(Expression<Func<TestModel[][], string>>)(model => model[i][j].Name),
// Calls that aren't indexers.
(Expression<Func<IList<TestModel>, string>>)(model => model.FirstOrDefault().Name),
(Expression<Func<IList<TestModel>, string>>)(model => model.FirstOrDefault().SelectedCategory.CategoryName.MainCategory),
(Expression<Func<IList<TestModel>, int>>)(model => model.FirstOrDefault().PreferredCategories.FirstOrDefault().CategoryId),
};
}
}
public static TheoryData<LambdaExpression, LambdaExpression> EquivalentExpressions
{
get
{
var value = "Test";
var Model = "Test";
return new TheoryData<LambdaExpression, LambdaExpression>
{
{
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory),
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory)
},
{
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName),
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName)
},
{
(Expression<Func<TestModel, int>>)(testModel => testModel.SelectedCategory.CategoryId),
(Expression<Func<TestModel, int>>)(testModel => testModel.SelectedCategory.CategoryId)
},
{
(Expression<Func<TestModel, string>>)(model => model.SelectedCategory.CategoryName.MainCategory),
(Expression<Func<TestModel, string>>)(model => model.SelectedCategory.CategoryName.MainCategory)
},
{
(Expression<Func<TestModel, TestModel>>)(model => model),
(Expression<Func<TestModel, TestModel>>)(m => m)
},
{
(Expression<Func<TestModel, string>>)(model => value),
(Expression<Func<TestModel, string>>)(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<Func<TestModel, string>>)(m => Model),
(Expression<Func<TestModel, TestModel>>)(m => m)
},
};
}
}
public static TheoryData<LambdaExpression, LambdaExpression> NonEquivalentExpressions
{
get
{
var value = "test";
var key = "TestModel";
var Model = "Test";
var myModel = new TestModel();
return new TheoryData<LambdaExpression, LambdaExpression>
{
{
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory),
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName)
},
{
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName),
(Expression<Func<LowerModel, CategoryName>>)(model => model.selectedcategory.CategoryName)
},
{
(Expression<Func<TestModel, string>>)(model => model.Model),
(Expression<Func<TestModel, string>>)(model => model.Name)
},
{
(Expression<Func<TestModel, string>>)(model => model.Model),
(Expression<Func<LowerModel, string>>)(model => model.model)
},
{
(Expression<Func<TestModel, string>>)(model => model.Name),
(Expression<Func<LowerModel, string>>)(model => model.name)
},
{
(Expression<Func<TestModel, CategoryName>>)(model => model.SelectedCategory.CategoryName),
(Expression<Func<TestModel, string>>)(model => value)
},
{
(Expression<Func<TestModel, string>>)(testModel => testModel.SelectedCategory.CategoryName.MainCategory),
(Expression<Func<TestModel, string>>)(testModel => value)
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
(Expression<Func<TestModel, string>>)(model => model.SelectedCategory.CategoryName.MainCategory)
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
(Expression<Func<IList<LowerModel>, Category>>)(model => model[2].selectedcategory)
},
{
(Expression<Func<TestModel, int>>)(testModel => testModel.SelectedCategory.CategoryId),
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory)
},
{
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory)
},
{
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
(Expression<Func<IDictionary<string, LowerModel>, string>>)(model => model[key].selectedcategory.CategoryName.MainCategory)
},
{
(Expression<Func<TestModel, string>>)(m => Model),
(Expression<Func<TestModel, string>>)(m => m.Model)
},
{
(Expression<Func<TestModel, TestModel>>)(m => m),
(Expression<Func<TestModel, string>>)(m => m.Model)
},
{
(Expression<Func<TestModel, string>>)(m => myModel.Name),
(Expression<Func<TestModel, string>>)(m => m.Name)
},
{
(Expression<Func<TestModel, string>>)(m => key),
(Expression<Func<TestModel, string>>)(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_DoesNotCacheIndexerOrUnsupportedExpression(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);
}
[Fact]
public void GetExpressionText_WithinALoop_ReturnsExpectedText()
{
// Arrange 0
var collection = new List<TestModel>();
for (var i = 0; i < 2; i++)
{
// Arrange i
var expectedText = $"collection[{i}].SelectedCategory.CategoryId";
// Act i
var result = ExpressionHelper.GetExpressionText(
(Expression<Func<List<TestModel>, int>>)(m => collection[i].SelectedCategory.CategoryId),
_expressionTextCache);
// Assert i
Assert.Equal(expectedText, result);
}
}
private class TestModel
{
public string Name { get; set; }
public string Model { get; set; }
public Category SelectedCategory { get; set; }
public IList<Category> PreferredCategories { get; set; }
}
private class LowerModel
{
public string name { get; set; }
public string model { get; set; }
public Category selectedcategory { get; set; }
public IList<Category> 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; }
}
}
|