| File: ExpressionHelper.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.ViewFeatures\src\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures) |
// 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.Diagnostics; using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; internal static class ExpressionHelper { public static string GetUncachedExpressionText(LambdaExpression expression) => GetExpressionText(expression, expressionTextCache: null); public static string GetExpressionText(LambdaExpression expression, ConcurrentDictionary<LambdaExpression, string> expressionTextCache) { ArgumentNullException.ThrowIfNull(expression); if (expressionTextCache != null && expressionTextCache.TryGetValue(expression, out var expressionText)) { return expressionText; } // Determine size of string needed (length) and number of segments it contains (segmentCount). Put another // way, segmentCount tracks the number of times the loop below should iterate. This avoids adding ".model" // and / or an extra leading "." and then removing them after the loop. Other information collected in this // first loop helps with length and segmentCount adjustments. doNotCache is somewhat separate: If // true, expression strings are not cached for the expression. // // After the corrections below the first loop, length is usually exactly the size of the returned string. // However when containsIndexers is true, the calculation is approximate because either evaluating indexer // expressions multiple times or saving indexer strings can get expensive. Optimizing for the common case // of a collection (not a dictionary) with less than 100 elements. If that assumption proves to be // incorrect, the StringBuilder will be enlarged but hopefully just once. var doNotCache = false; var lastIsModel = false; var length = 0; var segmentCount = 0; var trailingMemberExpressions = 0; var part = expression.Body; while (part != null) { switch (part.NodeType) { 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). doNotCache = true; lastIsModel = false; var methodExpression = (MethodCallExpression)part; if (IsSingleArgumentIndexer(methodExpression)) { length += "[99]".Length; part = methodExpression.Object; segmentCount++; trailingMemberExpressions = 0; } else { // Unsupported. part = null; } break; case ExpressionType.ArrayIndex: var binaryExpression = (BinaryExpression)part; doNotCache = true; lastIsModel = false; length += "[99]".Length; part = binaryExpression.Left; segmentCount++; trailingMemberExpressions = 0; break; case ExpressionType.MemberAccess: var memberExpressionPart = (MemberExpression)part; var name = memberExpressionPart.Member.Name; // If identifier contains "__", it is "reserved for use by the implementation" and likely // compiler- or Razor-generated e.g. the name of a field in a delegate's generated class. if (name.Contains("__")) { // Exit loop. part = null; } else { lastIsModel = string.Equals("model", name, StringComparison.OrdinalIgnoreCase); length += name.Length + 1; part = memberExpressionPart.Expression; segmentCount++; trailingMemberExpressions++; } break; case ExpressionType.Parameter: // Unsupported but indicates previous member access was not the view's Model. lastIsModel = false; part = null; break; default: // Unsupported. part = null; break; } } // If name would start with ".model", then strip that part away. if (lastIsModel) { length -= ".model".Length; segmentCount--; trailingMemberExpressions--; } // Trim the leading "." if present. The loop below special-cases the last property to avoid this addition. if (trailingMemberExpressions > 0) { length--; } Debug.Assert(segmentCount >= 0); if (segmentCount == 0) { Debug.Assert(!doNotCache); expressionTextCache?.TryAdd(expression, string.Empty); return string.Empty; } var builder = new StringBuilder(length); part = expression.Body; while (part != null && segmentCount > 0) { segmentCount--; switch (part.NodeType) { case ExpressionType.Call: Debug.Assert(doNotCache); var methodExpression = (MethodCallExpression)part; InsertIndexerInvocationText(builder, methodExpression.Arguments.Single(), expression); part = methodExpression.Object; break; case ExpressionType.ArrayIndex: Debug.Assert(doNotCache); var binaryExpression = (BinaryExpression)part; InsertIndexerInvocationText(builder, binaryExpression.Right, expression); part = binaryExpression.Left; break; case ExpressionType.MemberAccess: var memberExpression = (MemberExpression)part; var name = memberExpression.Member.Name; Debug.Assert(!name.Contains("__")); builder.Insert(0, name); if (segmentCount > 0) { // One or more parts to the left of this part are coming. builder.Insert(0, '.'); } part = memberExpression.Expression; break; default: // Should be unreachable due to handling in above loop. Debug.Assert(false); break; } } Debug.Assert(segmentCount == 0); expressionText = builder.ToString(); if (expressionTextCache != null && !doNotCache) { expressionTextCache.TryAdd(expression, expressionText); } return expressionText; } private static void InsertIndexerInvocationText( StringBuilder builder, Expression indexExpression, LambdaExpression parentExpression) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(indexExpression); ArgumentNullException.ThrowIfNull(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; try { func = CachedExpressionCompiler.Process(lambda) ?? lambda.Compile(); } catch (InvalidOperationException ex) { var parameters = parentExpression.Parameters.ToArray(); throw new InvalidOperationException( Resources.FormatExpressionHelper_InvalidIndexerExpression(indexExpression, parameters[0].Name), ex); } builder.Insert(0, ']'); builder.Insert(0, Convert.ToString(func(null), CultureInfo.InvariantCulture)); builder.Insert(0, '['); } public static bool IsSingleArgumentIndexer(Expression expression) { if (!(expression is MethodCallExpression methodExpression) || methodExpression.Arguments.Count != 1) { return false; } // Check whether GetDefaultMembers() (if present in CoreCLR) would return a member of this type. Compiler // names the indexer property, if any, in a generated [DefaultMember] attribute for the containing type. var declaringType = methodExpression.Method.DeclaringType; var defaultMember = declaringType.GetCustomAttribute<DefaultMemberAttribute>(inherit: true); if (defaultMember == null) { return false; } // Find default property (the indexer) and confirm its getter is the method in this expression. 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; } }