Do not cache expressions containing method calls

- #5655
- also make `ExpressionTextCache` more robust for defence-in-depth

nits:
- two `null` expression nodes are equal
- declare data properties as `TheoryData<T>`
This commit is contained in:
Doug Bunting 2017-01-05 20:38:58 -08:00
parent 842d661ac2
commit 8ee3d45ef1
3 changed files with 94 additions and 28 deletions

View File

@ -61,11 +61,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
switch (part.NodeType) switch (part.NodeType)
{ {
case ExpressionType.Call: case ExpressionType.Call:
// Will exit loop if at Method().Property or [i,j].Property. In that case (like [i].Property),
// don't cache and don't remove ".Model" (if that's .Property).
containsIndexers = true;
lastIsModel = false;
var methodExpression = (MethodCallExpression)part; var methodExpression = (MethodCallExpression)part;
if (IsSingleArgumentIndexer(methodExpression)) if (IsSingleArgumentIndexer(methodExpression))
{ {
containsIndexers = true;
lastIsModel = false;
length += "[99]".Length; length += "[99]".Length;
part = methodExpression.Object; part = methodExpression.Object;
segmentCount++; segmentCount++;

View File

@ -37,6 +37,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
while (true) while (true)
{ {
if (expression1 == null && expression2 == null)
{
return true;
}
if (expression1 == null || expression2 == null) if (expression1 == null || expression2 == null)
{ {
return false; return false;
@ -47,8 +52,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return false; return false;
} }
if (expression1.NodeType == ExpressionType.MemberAccess) switch (expression1.NodeType)
{ {
case ExpressionType.MemberAccess:
var memberExpression1 = (MemberExpression)expression1; var memberExpression1 = (MemberExpression)expression1;
var memberName1 = memberExpression1.Member.Name; var memberName1 = memberExpression1.Member.Name;
expression1 = memberExpression1.Expression; expression1 = memberExpression1.Expression;
@ -57,8 +63,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var memberName2 = memberExpression2.Member.Name; var memberName2 = memberExpression2.Member.Name;
expression2 = memberExpression2.Expression; expression2 = memberExpression2.Expression;
// If identifier contains "__", it is "reserved for use by the implementation" and likely compiler- // If identifier contains "__", it is "reserved for use by the implementation" and likely
// or Razor-generated e.g. the name of a field in a delegate's generated class. // compiler- or Razor-generated e.g. the name of a field in a delegate's generated class.
if (memberName1.Contains("__") && memberName2.Contains("__")) if (memberName1.Contains("__") && memberName2.Contains("__"))
{ {
return true; return true;
@ -68,9 +74,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
return false; return false;
} }
} break;
else
{ case ExpressionType.ArrayIndex:
// Shouldn't be cached. Just in case, ensure indexers are all different.
return false;
case ExpressionType.Call:
// Shouldn't be cached. Just in case, ensure indexers and other calls are all different.
return false;
default:
// Everything else terminates name generation. Haven't found a difference so far...
return true; return true;
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using Xunit; using Xunit;
@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
private readonly ExpressionTextCache _expressionTextCache = new ExpressionTextCache(); private readonly ExpressionTextCache _expressionTextCache = new ExpressionTextCache();
public static IEnumerable<object[]> ExpressionAndTexts public static TheoryData<Expression, string> ExpressionAndTexts
{ {
get get
{ {
@ -80,11 +81,35 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
(Expression<Func<IList<TestModel>, int>>)(model => model[2].PreferredCategories[i].CategoryId), (Expression<Func<IList<TestModel>, int>>)(model => model[2].PreferredCategories[i].CategoryId),
"[2].PreferredCategories[3].CategoryId" "[2].PreferredCategories[3].CategoryId"
}, },
{
(Expression<Func<IList<TestModel>, 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"
},
}; };
} }
} }
public static IEnumerable<object[]> CachedExpressions public static TheoryData<Expression> CachedExpressions
{ {
get get
{ {
@ -104,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
public static IEnumerable<object[]> IndexerExpressions public static TheoryData<Expression> IndexerExpressions
{ {
get get
{ {
@ -123,7 +148,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
public static IEnumerable<object[]> EquivalentExpressions public static TheoryData<Expression> UnsupportedExpressions
{
get
{
var i = 2;
var j = 3;
return new TheoryData<Expression>
{
// 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<Expression, Expression> EquivalentExpressions
{ {
get get
{ {
@ -167,7 +214,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
public static IEnumerable<object[]> NonEquivalentExpressions public static TheoryData<Expression, Expression> NonEquivalentExpressions
{ {
get get
{ {
@ -253,7 +300,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
[Theory] [Theory]
[MemberData(nameof(IndexerExpressions))] [MemberData(nameof(IndexerExpressions))]
public void GetExpressionText_DoesNotCacheIndexerExpression(LambdaExpression expression) [MemberData(nameof(UnsupportedExpressions))]
public void GetExpressionText_DoesNotCacheIndexerOrUnspportedExpression(LambdaExpression expression)
{ {
// Act - 1 // Act - 1
var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache);