public static string GetExpressionText(LambdaExpression expression, ExpressionTextCache expressionTextCache) { if (expression == null) { throw new ArgumentNullException(nameof(expression)); } if (expressionTextCache != null && expressionTextCache.Entries.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); if (expressionTextCache != null) { expressionTextCache.Entries.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.Entries.TryAdd(expression, expressionText); } return(expressionText); }
public static string GetExpressionText(LambdaExpression expression, ExpressionTextCache expressionTextCache) { if (expression == null) { throw new ArgumentNullException(nameof(expression)); } string expressionText; if (expressionTextCache != null && expressionTextCache.Entries.TryGetValue(expression, out expressionText)) { return(expressionText); } var containsIndexers = false; 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) { containsIndexers = true; var methodExpression = (MethodCallExpression)part; if (!IsSingleArgumentIndexer(methodExpression)) { // Unsupported. break; } InsertIndexerInvocationText( builder, methodExpression.Arguments.Single(), expression); part = methodExpression.Object; } else if (part.NodeType == ExpressionType.ArrayIndex) { containsIndexers = true; var binaryExpression = (BinaryExpression)part; InsertIndexerInvocationText( builder, binaryExpression.Right, expression); part = binaryExpression.Left; } else if (part.NodeType == 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. Should have the entire name because previous MemberAccess has same name as the // leftmost expression node (a variable). break; } builder.Insert(0, name); builder.Insert(0, '.'); part = memberExpressionPart.Expression; } else { break; } } // If parts start with "model", then strip that part away. if (part == null || part.NodeType != ExpressionType.Parameter) { var text = builder.ToString(); if (text.StartsWith(".model", StringComparison.OrdinalIgnoreCase)) { // 6 is the length of the string ".model". builder.Remove(0, 6); } } if (builder.Length > 0) { // Trim the leading "." if present. builder.Replace(".", string.Empty, 0, 1); } expressionText = builder.ToString(); if (expressionTextCache != null && !containsIndexers) { expressionTextCache.Entries.TryAdd(expression, expressionText); } return(expressionText); }