Files
Umbraco-CMS/src/Umbraco.Core/ExpressionHelper.cs
Nikolaj Brask-Nielsen 6d48091328 docs: XML warnings (#14663)
* chore: Fix XML warnings

* docs: Fix XML warnings

* docs: Fix XML in resource designer

* docs: Fix XML warnings

* Revert "docs: Fix XML in resource designer"

This reverts commit 8ea61c51ac161e1853ae080db7fe1b4d4cb4d2be.
2023-09-06 20:08:17 +02:00

442 lines
16 KiB
C#

using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using Umbraco.Cms.Core.Persistence;
namespace Umbraco.Cms.Core;
/// <summary>
/// A set of helper methods for dealing with expressions
/// </summary>
/// <remarks></remarks>
public static class ExpressionHelper
{
private static readonly ConcurrentDictionary<LambdaExpressionCacheKey, PropertyInfo> PropertyInfoCache = new();
/// <summary>
/// Gets a <see cref="PropertyInfo" /> object from an expression.
/// </summary>
/// <typeparam name="TSource">The type of the source.</typeparam>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="source">The source.</param>
/// <param name="propertyLambda">The property lambda.</param>
/// <returns></returns>
/// <remarks></remarks>
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(
this TSource source,
Expression<Func<TSource, TProperty>> propertyLambda) => GetPropertyInfo(propertyLambda);
/// <summary>
/// Gets a <see cref="PropertyInfo" /> object from an expression.
/// </summary>
/// <typeparam name="TSource">The type of the source.</typeparam>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="propertyLambda">The property lambda.</param>
/// <returns></returns>
/// <remarks></remarks>
public static PropertyInfo
GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) =>
PropertyInfoCache.GetOrAdd(
new LambdaExpressionCacheKey(propertyLambda),
x =>
{
Type type = typeof(TSource);
var member = propertyLambda.Body as MemberExpression;
if (member == null)
{
if (propertyLambda.Body.GetType().Name == "UnaryExpression")
{
// The expression might be for some boxing, e.g. representing a value type like HiveId as an object
// in which case the expression will be Convert(x.MyProperty)
if (propertyLambda.Body is UnaryExpression unary)
{
if (unary.Operand is not MemberExpression boxedMember)
{
throw new ArgumentException(
"The type of property could not be inferred, try specifying the type parameters explicitly. This can happen if you have tried to access PropertyInfo where the property's return type is a value type, but the expression is trying to convert it to an object");
}
member = boxedMember;
}
}
else
{
throw new ArgumentException(
string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda));
}
}
var propInfo = member!.Member as PropertyInfo;
if (propInfo == null)
{
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a field, not a property.",
propertyLambda));
}
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType!))
{
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a property that is not from type {1}.",
propertyLambda,
type));
}
return propInfo;
});
public static (MemberInfo, string?) FindProperty(LambdaExpression lambda)
{
void Throw()
{
throw new ArgumentException(
$"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.",
nameof(lambda));
}
Expression expr = lambda;
var loop = true;
string? alias = null;
while (loop)
{
switch (expr.NodeType)
{
case ExpressionType.Convert:
expr = ((UnaryExpression)expr).Operand;
break;
case ExpressionType.Lambda:
expr = ((LambdaExpression)expr).Body;
break;
case ExpressionType.Call:
var callExpr = (MethodCallExpression)expr;
MethodInfo method = callExpr.Method;
if (method.DeclaringType != typeof(SqlExtensionsStatics) || method.Name != "Alias" ||
!(callExpr.Arguments[1] is ConstantExpression aliasExpr))
{
Throw();
}
expr = callExpr.Arguments[0];
alias = aliasExpr.Value?.ToString();
break;
case ExpressionType.MemberAccess:
var memberExpr = (MemberExpression)expr;
if (memberExpr.Expression?.NodeType != ExpressionType.Parameter &&
memberExpr.Expression?.NodeType != ExpressionType.Convert)
{
Throw();
}
return (memberExpr.Member, alias);
default:
loop = false;
break;
}
}
throw new Exception("Configuration for members is only supported for top-level individual members on a type.");
}
public static IDictionary<string, object?>? GetMethodParams<T1, T2>(Expression<Func<T1, T2>> fromExpression)
{
if (fromExpression == null)
{
return null;
}
if (fromExpression.Body is not MethodCallExpression body)
{
return new Dictionary<string, object?>();
}
var rVal = new Dictionary<string, object?>();
var parameters = body.Method.GetParameters().Select(x => x.Name).Where(x => x is not null).ToArray();
var i = 0;
foreach (Expression argument in body.Arguments)
{
LambdaExpression lambda = Expression.Lambda(argument, fromExpression.Parameters);
Delegate d = lambda.Compile();
var value = d.DynamicInvoke(new object[1]);
rVal.Add(parameters[i]!, value);
i++;
}
return rVal;
}
/// <summary>
/// Gets a <see cref="MethodInfo" /> from an <see cref="Expression{TDelegate}"/> of <see cref="Action{T}"/> provided it refers to a method call.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="fromExpression">From expression.</param>
/// <returns>
/// The <see cref="MethodInfo" /> or null if <paramref name="fromExpression" /> is null or cannot be converted to
/// <see cref="MethodCallExpression" />.
/// </returns>
/// <remarks></remarks>
public static MethodInfo? GetMethodInfo<T>(Expression<Action<T>> fromExpression)
{
if (fromExpression == null)
{
return null;
}
var body = fromExpression.Body as MethodCallExpression;
return body?.Method;
}
/// <summary>
/// Gets the method info.
/// </summary>
/// <typeparam name="TReturn">The return type of the method.</typeparam>
/// <param name="fromExpression">From expression.</param>
/// <returns></returns>
public static MethodInfo? GetMethodInfo<TReturn>(Expression<Func<TReturn>> fromExpression)
{
if (fromExpression == null)
{
return null;
}
var body = fromExpression.Body as MethodCallExpression;
return body?.Method;
}
/// <summary>
/// Gets the method info.
/// </summary>
/// <typeparam name="T1">The type of the 1.</typeparam>
/// <typeparam name="T2">The type of the 2.</typeparam>
/// <param name="fromExpression">From expression.</param>
/// <returns></returns>
public static MethodInfo? GetMethodInfo<T1, T2>(Expression<Func<T1, T2>> fromExpression)
{
if (fromExpression == null)
{
return null;
}
MethodCallExpression? me;
switch (fromExpression.Body.NodeType)
{
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
var ue = fromExpression.Body as UnaryExpression;
me = ue?.Operand as MethodCallExpression;
break;
default:
me = fromExpression.Body as MethodCallExpression;
break;
}
return me?.Method;
}
/// <summary>
/// Gets a <see cref="MethodInfo" /> from an <see cref="Expression" /> provided it refers to a method call.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>
/// The <see cref="MethodInfo" /> or null if <paramref name="expression" /> cannot be converted to
/// <see cref="MethodCallExpression" />.
/// </returns>
/// <remarks></remarks>
public static MethodInfo? GetMethod(Expression expression)
{
if (expression == null)
{
return null;
}
return IsMethod(expression) ? ((MethodCallExpression)expression).Method : null;
}
/// <summary>
/// Gets a <see cref="MemberInfo" /> from an <see cref="Expression{TDelegate}" /> of <see cref="Func{T, TReturn}"/> provided it refers to member
/// access.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TReturn">The type of the return.</typeparam>
/// <param name="fromExpression">From expression.</param>
/// <returns>
/// The <see cref="MemberInfo" /> or null if <paramref name="fromExpression" /> cannot be converted to
/// <see cref="MemberExpression" />.
/// </returns>
/// <remarks></remarks>
public static MemberInfo? GetMemberInfo<T, TReturn>(Expression<Func<T, TReturn>> fromExpression)
{
if (fromExpression == null)
{
return null;
}
MemberExpression? me;
switch (fromExpression.Body.NodeType)
{
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
var ue = fromExpression.Body as UnaryExpression;
me = ue?.Operand as MemberExpression;
break;
default:
me = fromExpression.Body as MemberExpression;
break;
}
return me?.Member;
}
/// <summary>
/// Determines whether the MethodInfo is the same based on signature, not based on the equality operator or HashCode.
/// </summary>
/// <param name="left">The left.</param>
/// <param name="right">The right.</param>
/// <returns>
/// <c>true</c> if [is method signature equal to] [the specified left]; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This is useful for comparing Expression methods that may contain different generic types
/// </remarks>
public static bool IsMethodSignatureEqualTo(this MethodInfo left, MethodInfo right)
{
if (left.Equals(right))
{
return true;
}
if (left.DeclaringType != right.DeclaringType)
{
return false;
}
if (left.Name != right.Name)
{
return false;
}
ParameterInfo[] leftParams = left.GetParameters();
ParameterInfo[] rightParams = right.GetParameters();
if (leftParams.Length != rightParams.Length)
{
return false;
}
for (var i = 0; i < leftParams.Length; i++)
{
// if they are delegate parameters, then assume they match as they could be anything
if (typeof(Delegate).IsAssignableFrom(leftParams[i].ParameterType) &&
typeof(Delegate).IsAssignableFrom(rightParams[i].ParameterType))
{
continue;
}
// if they are not delegates, then compare the types
if (leftParams[i].ParameterType != rightParams[i].ParameterType)
{
return false;
}
}
if (left.ReturnType != right.ReturnType)
{
return false;
}
return true;
}
/// <summary>
/// Gets a <see cref="MemberInfo" /> from an <see cref="Expression" /> provided it refers to member access.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns></returns>
/// <remarks></remarks>
public static MemberInfo? GetMember(Expression expression)
{
if (expression == null)
{
return null;
}
return IsMember(expression) ? ((MemberExpression)expression).Member : null;
}
/// <summary>
/// Gets a <see cref="MethodInfo" /> from a <see cref="Delegate" />
/// </summary>
/// <param name="fromMethodGroup">From method group.</param>
/// <returns></returns>
/// <remarks></remarks>
public static MethodInfo GetStaticMethodInfo(Delegate fromMethodGroup)
{
if (fromMethodGroup == null)
{
throw new ArgumentNullException("fromMethodGroup");
}
return fromMethodGroup.Method;
}
///// <summary>
///// Formats an unhandled item for representing the expression as a string.
///// </summary>
///// <typeparam name="T"></typeparam>
///// <param name="unhandledItem">The unhandled item.</param>
///// <returns></returns>
///// <remarks></remarks>
// public static string FormatUnhandledItem<T>(T unhandledItem) where T : class
// {
// if (unhandledItem == null) throw new ArgumentNullException("unhandledItem");
// var itemAsExpression = unhandledItem as Expression;
// return itemAsExpression != null
// ? FormattingExpressionTreeVisitor.Format(itemAsExpression)
// : unhandledItem.ToString();
// }
/// <summary>
/// Determines whether the specified expression is a method.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns><c>true</c> if the specified expression is method; otherwise, <c>false</c>.</returns>
/// <remarks></remarks>
public static bool IsMethod(Expression expression) => expression is MethodCallExpression;
/// <summary>
/// Determines whether the specified expression is a member.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns><c>true</c> if the specified expression is member; otherwise, <c>false</c>.</returns>
/// <remarks></remarks>
public static bool IsMember(Expression expression) => expression is MemberExpression;
/// <summary>
/// Determines whether the specified expression is a constant.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns><c>true</c> if the specified expression is constant; otherwise, <c>false</c>.</returns>
/// <remarks></remarks>
public static bool IsConstant(Expression expression) => expression is ConstantExpression;
/// <summary>
/// Gets the first value from the supplied arguments of an expression, for those arguments that can be cast to
/// <see cref="ConstantExpression" />.
/// </summary>
/// <param name="arguments">The arguments.</param>
/// <returns></returns>
/// <remarks></remarks>
public static object? GetFirstValueFromArguments(IEnumerable<Expression> arguments)
{
if (arguments == null)
{
return false;
}
return
arguments.Where(x => x is ConstantExpression).Cast
<ConstantExpression>().Select(x => x.Value).DefaultIfEmpty(null).FirstOrDefault();
}
}