diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/CachedExpressionCompiler.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/CachedExpressionCompiler.cs new file mode 100644 index 0000000000..a0625601e9 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/CachedExpressionCompiler.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace System.Web.Mvc.ExpressionUtil +{ + internal static class CachedExpressionCompiler + { + // This is the entry point to the cached expression compilation system. The system + // will try to turn the expression into an actual delegate as quickly as possible, + // relying on cache lookups and other techniques to save time if appropriate. + // If the provided expression is particularly obscure and the system doesn't know + // how to handle it, we'll just compile the expression as normal. + public static Func Process(Expression> lambdaExpression) + { + return Compiler.Compile(lambdaExpression); + } + + private static class Compiler + { + private static Func _identityFunc; + + private static readonly ConcurrentDictionary> _simpleMemberAccessDict = + new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> _constMemberAccessDict = + new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> _fingerprintedCache = + new ConcurrentDictionary>(); + + public static Func Compile(Expression> expr) + { + return CompileFromIdentityFunc(expr) + ?? CompileFromConstLookup(expr) + ?? CompileFromMemberAccess(expr) + ?? CompileFromFingerprint(expr) + ?? CompileSlow(expr); + } + + private static Func CompileFromConstLookup(Expression> expr) + { + ConstantExpression constExpr = expr.Body as ConstantExpression; + if (constExpr != null) + { + // model => {const} + + TOut constantValue = (TOut)constExpr.Value; + return _ => constantValue; + } + + return null; + } + + private static Func CompileFromIdentityFunc(Expression> expr) + { + if (expr.Body == expr.Parameters[0]) + { + // model => model + + // don't need to lock, as all identity funcs are identical + if (_identityFunc == null) + { + _identityFunc = expr.Compile(); + } + + return _identityFunc; + } + + return null; + } + + private static Func CompileFromFingerprint(Expression> expr) + { + List capturedConstants; + ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants); + + if (fingerprint != null) + { + var del = _fingerprintedCache.GetOrAdd(fingerprint, _ => + { + // Fingerprinting succeeded, but there was a cache miss. Rewrite the expression + // and add the rewritten expression to the cache. + + var hoistedExpr = HoistingExpressionVisitor.Hoist(expr); + return hoistedExpr.Compile(); + }); + return model => del(model, capturedConstants); + } + + // couldn't be fingerprinted + return null; + } + + private static Func CompileFromMemberAccess(Expression> expr) + { + // Performance tests show that on the x64 platform, special-casing static member and + // captured local variable accesses is faster than letting the fingerprinting system + // handle them. On the x86 platform, the fingerprinting system is faster, but only + // by around one microsecond, so it's not worth it to complicate the logic here with + // an architecture check. + + MemberExpression memberExpr = expr.Body as MemberExpression; + if (memberExpr != null) + { + if (memberExpr.Expression == expr.Parameters[0] || memberExpr.Expression == null) + { + // model => model.Member or model => StaticMember + return _simpleMemberAccessDict.GetOrAdd(memberExpr.Member, _ => expr.Compile()); + } + + ConstantExpression constExpr = memberExpr.Expression as ConstantExpression; + if (constExpr != null) + { + // model => {const}.Member (captured local variable) + var del = _constMemberAccessDict.GetOrAdd(memberExpr.Member, _ => + { + // rewrite as capturedLocal => ((TDeclaringType)capturedLocal).Member + var constParamExpr = Expression.Parameter(typeof(object), "capturedLocal"); + var constCastExpr = Expression.Convert(constParamExpr, memberExpr.Member.DeclaringType); + var newMemberAccessExpr = memberExpr.Update(constCastExpr); + var newLambdaExpr = Expression.Lambda>(newMemberAccessExpr, constParamExpr); + return newLambdaExpr.Compile(); + }); + + object capturedLocal = constExpr.Value; + return _ => del(capturedLocal); + } + } + + return null; + } + + private static Func CompileSlow(Expression> expr) + { + // fallback compilation system - just compile the expression directly + return expr.Compile(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionHelper.cs new file mode 100644 index 0000000000..be91bcd8bc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Expressions/ExpressionHelper.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Web.Mvc.ExpressionUtil; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public static class ExpressionHelper + { + public static string GetExpressionText(string expression) + { + return + String.Equals(expression, "model", StringComparison.OrdinalIgnoreCase) + ? String.Empty // If it's exactly "model", then give them an empty string, to replicate the lambda behavior + : expression; + } + + public static string GetExpressionText(LambdaExpression expression) + { + // Split apart the expression string for property/field accessors to create its name + Stack nameParts = new Stack(); + Expression part = expression.Body; + + while (part != null) + { + if (part.NodeType == ExpressionType.Call) + { + MethodCallExpression methodExpression = (MethodCallExpression)part; + + if (!IsSingleArgumentIndexer(methodExpression)) + { + break; + } + + nameParts.Push( + GetIndexerInvocation( + methodExpression.Arguments.Single(), + expression.Parameters.ToArray())); + + part = methodExpression.Object; + } + else if (part.NodeType == ExpressionType.ArrayIndex) + { + BinaryExpression binaryExpression = (BinaryExpression)part; + + nameParts.Push( + GetIndexerInvocation( + binaryExpression.Right, + expression.Parameters.ToArray())); + + part = binaryExpression.Left; + } + else if (part.NodeType == ExpressionType.MemberAccess) + { + MemberExpression memberExpressionPart = (MemberExpression)part; + nameParts.Push("." + memberExpressionPart.Member.Name); + part = memberExpressionPart.Expression; + } + else if (part.NodeType == ExpressionType.Parameter) + { + // Dev10 Bug #907611 + // 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); + part = null; + } + else + { + break; + } + } + + // If it starts with "model", then strip that away + if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase)) + { + nameParts.Pop(); + } + + if (nameParts.Count > 0) + { + return nameParts.Aggregate((left, right) => left + right).TrimStart('.'); + } + + return String.Empty; + } + + private static string GetIndexerInvocation(Expression expression, ParameterExpression[] parameters) + { + Expression converted = Expression.Convert(expression, typeof(object)); + ParameterExpression fakeParameter = Expression.Parameter(typeof(object), null); + Expression> lambda = Expression.Lambda>(converted, fakeParameter); + Func func; + + try + { + func = CachedExpressionCompiler.Process(lambda); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ExpressionHelper_InvalidIndexerExpression, + expression, + parameters[0].Name), + ex); + } + + return "[" + Convert.ToString(func(null), CultureInfo.InvariantCulture) + "]"; + } + + internal static bool IsSingleArgumentIndexer(Expression expression) + { + MethodCallExpression methodExpression = expression as MethodCallExpression; + if (methodExpression == null || methodExpression.Arguments.Count != 1) + { + return false; + } + + return methodExpression.Method + .DeclaringType + .GetDefaultMembers() + .OfType() + .Any(p => p.GetGetMethod() == methodExpression.Method); + } + } +}