[Perf] Rewrite ExpressionHelper.GetExpressionText using StringBuilder

This commit is contained in:
mnltejaswini 2016-03-25 10:23:32 -07:00
parent de9ffb13c7
commit 7fbd407ad5
2 changed files with 147 additions and 42 deletions

View File

@ -2,11 +2,11 @@
// 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.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
@ -38,10 +38,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
var containsIndexers = false;
// Split apart the expression string for property/field accessors to create its name
var nameParts = new Stack<string>();
var part = expression.Body;
// Builder to concatenate the names for property/field accessors within an expression to create a string.
var builder = new StringBuilder();
while (part != null)
{
if (part.NodeType == ExpressionType.Call)
@ -54,10 +55,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
break;
}
nameParts.Push(
GetIndexerInvocation(
methodExpression.Arguments.Single(),
expression.Parameters.ToArray()));
InsertIndexerInvocationText(
builder,
methodExpression.Arguments.Single(),
expression);
part = methodExpression.Object;
}
@ -66,10 +67,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
containsIndexers = true;
var binaryExpression = (BinaryExpression)part;
nameParts.Push(
GetIndexerInvocation(
binaryExpression.Right,
expression.Parameters.ToArray()));
InsertIndexerInvocationText(
builder,
binaryExpression.Right,
expression);
part = binaryExpression.Left;
}
@ -87,20 +88,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
break;
}
nameParts.Push("." + name);
builder.Insert(0, name);
builder.Insert(0, '.');
part = memberExpressionPart.Expression;
}
else if (part.NodeType == ExpressionType.Parameter)
{
// When the expression is parameter based (m => m.Something...), we'll push an empty
// string onto the stack and stop evaluating. The extra empty string makes sure that
// we don't accidentally cut off too much of m => m.Model.
nameParts.Push(string.Empty);
// Exit loop. Have the entire name because the parameter cannot be used as an indexer; always the
// leftmost expression node.
break;
}
else
{
break;
@ -108,17 +99,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
// If parts start with "model", then strip that part away.
if (nameParts.Count > 0 && string.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase))
if (part == null || part.NodeType != ExpressionType.Parameter)
{
nameParts.Pop();
var text = builder.ToString();
if (text.StartsWith(".model", StringComparison.OrdinalIgnoreCase))
{
// 6 is the length of the string ".model".
builder.Remove(0, 6);
}
}
expressionText = string.Empty;
if (nameParts.Count > 0)
if (builder.Length > 0)
{
expressionText = nameParts.Aggregate((left, right) => left + right).TrimStart('.');
// Trim the leading "." if present.
builder.Replace(".", string.Empty, 0, 1);
}
expressionText = builder.ToString();
if (expressionTextCache != null && !containsIndexers)
{
expressionTextCache.Entries.TryAdd(expression, expressionText);
@ -127,21 +125,34 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return expressionText;
}
private static string GetIndexerInvocation(
Expression expression,
ParameterExpression[] parameters)
private static void InsertIndexerInvocationText(
StringBuilder builder,
Expression indexExpression,
LambdaExpression parentExpression)
{
if (expression == null)
if (builder == null)
{
throw new ArgumentNullException(nameof(expression));
throw new ArgumentNullException(nameof(builder));
}
if (parameters == null)
if (indexExpression == null)
{
throw new ArgumentNullException(nameof(parameters));
throw new ArgumentNullException(nameof(indexExpression));
}
var converted = Expression.Convert(expression, typeof(object));
if (parentExpression == null)
{
throw new ArgumentNullException(nameof(parentExpression));
}
if (parentExpression.Parameters == null)
{
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(parentExpression.Parameters),
nameof(parentExpression)));
}
var converted = Expression.Convert(indexExpression, typeof(object));
var fakeParameter = Expression.Parameter(typeof(object), null);
var lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
Func<object, object> func;
@ -152,12 +163,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
catch (InvalidOperationException ex)
{
var parameters = parentExpression.Parameters.ToArray();
throw new InvalidOperationException(
Resources.FormatExpressionHelper_InvalidIndexerExpression(expression, parameters[0].Name),
Resources.FormatExpressionHelper_InvalidIndexerExpression(indexExpression, parameters[0].Name),
ex);
}
return "[" + Convert.ToString(func(null), CultureInfo.InvariantCulture) + "]";
builder.Insert(0, ']');
builder.Insert(0, Convert.ToString(func(null), CultureInfo.InvariantCulture));
builder.Insert(0, '[');
}
public static bool IsSingleArgumentIndexer(Expression expression)
@ -178,9 +192,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
// Find default property (the indexer) and confirm its getter is the method in this expression.
return declaringType.GetRuntimeProperties().Any(
property => (string.Equals(defaultMember.MemberName, property.Name, StringComparison.Ordinal) &&
property.GetMethod == methodExpression.Method));
var runtimeProperties = declaringType.GetRuntimeProperties();
foreach (var property in runtimeProperties)
{
if ((string.Equals(defaultMember.MemberName, property.Name, StringComparison.Ordinal) &&
property.GetMethod == methodExpression.Method))
{
return true;
}
}
return false;
}
}
}

View File

@ -12,6 +12,78 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
private readonly ExpressionTextCache _expressionTextCache = new ExpressionTextCache();
public static IEnumerable<object[]> ExpressionAndTexts
{
get
{
var i = 3;
var value = "Test";
var Model = new TestModel();
var key = "TestModel";
var myModels = new List<TestModel>();
return new TheoryData<Expression, string>
{
{
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory),
"SelectedCategory"
},
{
(Expression<Func<TestModel, Category>>)(m => 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<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, TestModel>>)(m => Model),
string.Empty
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
"[2].SelectedCategory"
},
{
(Expression<Func<IList<TestModel>, 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"
},
};
}
}
public static IEnumerable<object[]> CachedExpressions
{
get
@ -162,6 +234,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
}
[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)
@ -235,7 +318,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
private class CategoryName
{
public string MainCategory { get; set; }
public string MainCategory { get; set; }
public string SubCategory { get; set; }
}
}