Получите свойство, как строку, из выражения <Func <TModel, TProperty >>
Я использую некоторые сильно типизированные выражения, которые сериализуются, чтобы мой код пользовательского интерфейса имел строго типизированные выражения для сортировки и поиска. Они имеют тип Expression<Func<TModel,TProperty>>
и используются как таковые: SortOption.Field = (p => p.FirstName);
. Я получил эту работу отлично для этого простого случая.
Код, который я использую для разбора свойства FirstName, фактически повторно использует некоторые существующие функции в стороннем продукте, который мы используем, и он отлично работает, пока мы не начнем работать с глубоко вложенными свойствами ( SortOption.Field = (p => p.Address.State.Abbreviation);
). Этот код имеет несколько разных предположений о необходимости поддержки глубоко вложенных свойств.
Что касается этого кода, я действительно не понимаю его и вместо того, чтобы изменять этот код, я решил, что должен просто написать с нуля эту функциональность. Однако я не знаю, как это сделать. Я подозреваю, что мы можем сделать что-то лучше, чем делать ToString() и выполнять синтаксический анализ строк. Итак, какой хороший способ сделать это для обработки тривиальных и глубоко вложенных случаев?
Требования:
- Для выражения
p => p.FirstName
мне нужна строка "FirstName"
.
- Для выражения
p => p.Address.State.Abbreviation
мне нужна строка "Address.State.Abbreviation"
Хотя это не важно для ответа на мой вопрос, я подозреваю, что мой код сериализации/десериализации может быть полезен кому-то другому, который находит этот вопрос в будущем, поэтому он ниже. Опять же, этот код не важен для вопроса - я просто подумал, что это может помочь кому-то. Обратите внимание, что DynamicExpression.ParseLambda
поступает из Динамический LINQ и Property.PropertyToString()
, о чем идет речь.
/// <summary>
/// This defines a framework to pass, across serialized tiers, sorting logic to be performed.
/// </summary>
/// <typeparam name="TModel">This is the object type that you are filtering.</typeparam>
/// <typeparam name="TProperty">This is the property on the object that you are filtering.</typeparam>
[Serializable]
public class SortOption<TModel, TProperty> : ISerializable where TModel : class
{
/// <summary>
/// Convenience constructor.
/// </summary>
/// <param name="property">The property to sort.</param>
/// <param name="isAscending">Indicates if the sorting should be ascending or descending</param>
/// <param name="priority">Indicates the sorting priority where 0 is a higher priority than 10.</param>
public SortOption(Expression<Func<TModel, TProperty>> property, bool isAscending = true, int priority = 0)
{
Property = property;
IsAscending = isAscending;
Priority = priority;
}
/// <summary>
/// Default Constructor.
/// </summary>
public SortOption()
: this(null)
{
}
/// <summary>
/// This is the field on the object to filter.
/// </summary>
public Expression<Func<TModel, TProperty>> Property { get; set; }
/// <summary>
/// This indicates if the sorting should be ascending or descending.
/// </summary>
public bool IsAscending { get; set; }
/// <summary>
/// This indicates the sorting priority where 0 is a higher priority than 10.
/// </summary>
public int Priority { get; set; }
#region Implementation of ISerializable
/// <summary>
/// This is the constructor called when deserializing a SortOption.
/// </summary>
protected SortOption(SerializationInfo info, StreamingContext context)
{
IsAscending = info.GetBoolean("IsAscending");
Priority = info.GetInt32("Priority");
// We just persisted this by the PropertyName. So let rebuild the Lambda Expression from that.
Property = DynamicExpression.ParseLambda<TModel, TProperty>(info.GetString("Property"), default(TModel), default(TProperty));
}
/// <summary>
/// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"/> with the data needed to serialize the target object.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"/> to populate with data. </param>
/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"/>) for this serialization. </param>
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
// Just stick the property name in there. We'll rebuild the expression based on that on the other end.
info.AddValue("Property", Property.PropertyToString());
info.AddValue("IsAscending", IsAscending);
info.AddValue("Priority", Priority);
}
#endregion
}
Ответы
Ответ 1
Вот трюк: любое выражение этой формы...
obj => obj.A.B.C // etc.
... на самом деле это всего лишь куча вложенных объектов MemberExpression
.
Сначала у вас есть:
MemberExpression: obj.A.B.C
Expression: obj.A.B // MemberExpression
Member: C
Оценка Expression
выше как MemberExpression
дает вам:
MemberExpression: obj.A.B
Expression: obj.A // MemberExpression
Member: B
Наконец, над этим (в верхней части) вы:
MemberExpression: obj.A
Expression: obj // note: not a MemberExpression
Member: A
Итак, кажется очевидным, что подход к этой проблеме заключается в проверке свойства Expression
объекта MemberExpression
до момента, когда он уже не является MemberExpression
.
ОБНОВЛЕНИЕ. Похоже, что ваша проблема добавлена. Возможно, у вас есть лямбда, которая выглядит как Func<T, int>
...
p => p.Age
... но на самом деле a Func<T, object>
; в этом случае компилятор преобразует вышеуказанное выражение в:
p => Convert(p.Age)
Настройка этой проблемы на самом деле не такая сложная, как может показаться. Взгляните на мой обновленный код для того, чтобы справиться с ним. Обратите внимание, что, абстрагируя код для получения MemberExpression
в свой собственный метод (TryFindMemberExpression
), этот подход сохраняет метод GetFullPropertyName
достаточно чистым и позволяет добавлять дополнительные проверки в будущем - если, возможно, вы столкнуться с новым сценарием, который вы изначально не учитывали, - не пропуская слишком много кода.
Чтобы проиллюстрировать: этот код работал у меня.
// code adjusted to prevent horizontal overflow
static string GetFullPropertyName<T, TProperty>
(Expression<Func<T, TProperty>> exp)
{
MemberExpression memberExp;
if (!TryFindMemberExpression(exp.Body, out memberExp))
return string.Empty;
var memberNames = new Stack<string>();
do
{
memberNames.Push(memberExp.Member.Name);
}
while (TryFindMemberExpression(memberExp.Expression, out memberExp));
return string.Join(".", memberNames.ToArray());
}
// code adjusted to prevent horizontal overflow
private static bool TryFindMemberExpression
(Expression exp, out MemberExpression memberExp)
{
memberExp = exp as MemberExpression;
if (memberExp != null)
{
// heyo! that was easy enough
return true;
}
// if the compiler created an automatic conversion,
// it'll look something like...
// obj => Convert(obj.Property) [e.g., int -> object]
// OR:
// obj => ConvertChecked(obj.Property) [e.g., int -> long]
// ...which are the cases checked in IsConversion
if (IsConversion(exp) && exp is UnaryExpression)
{
memberExp = ((UnaryExpression)exp).Operand as MemberExpression;
if (memberExp != null)
{
return true;
}
}
return false;
}
private static bool IsConversion(Expression exp)
{
return (
exp.NodeType == ExpressionType.Convert ||
exp.NodeType == ExpressionType.ConvertChecked
);
}
Использование:
Expression<Func<Person, string>> simpleExp = p => p.FirstName;
Expression<Func<Person, string>> complexExp = p => p.Address.State.Abbreviation;
Expression<Func<Person, object>> ageExp = p => p.Age;
Console.WriteLine(GetFullPropertyName(simpleExp));
Console.WriteLine(GetFullPropertyName(complexExp));
Console.WriteLine(GetFullPropertyName(ageExp));
Вывод:
FirstName
Address.State.Abbreviation
Age
Ответ 2
Вот метод, который позволяет получить строковое представление, даже если у вас есть вложенные свойства:
public static string GetPropertySymbol<T,TResult>(Expression<Func<T,TResult>> expression)
{
return String.Join(".",
GetMembersOnPath(expression.Body as MemberExpression)
.Select(m => m.Member.Name)
.Reverse());
}
private static IEnumerable<MemberExpression> GetMembersOnPath(MemberExpression expression)
{
while(expression != null)
{
yield return expression;
expression = expression.Expression as MemberExpression;
}
}
Если вы все еще используете .NET 3.5, вам нужно прикрепить ToArray()
после вызова Reverse()
, потому что перегрузка String.Join
, которая принимает IEnumerable
, была впервые добавлена в .NET 4.
Ответ 3
Для "FirstName"
из p => p.FirstName
Expression<Func<TModel, TProperty>> expression; //your given expression
string fieldName = ((MemberExpression)expression.Body).Member.Name; //watch out for runtime casting errors
Я предлагаю вам проверить код ASP.NET MVC 2 (из aspnet.codeplex.com), так как он имеет похожий API для помощников HTML... Html.TextBoxFor( p => p.FirstName )
т.д.
Ответ 4
Другой простой подход заключается в использовании метода System.Web.Mvc.ExpressionHelper.GetExpressionText. В своем следующем ударе я напишу более подробно. Загляните на http://carrarini.blogspot.com/.
Ответ 5
Я написал для этого небольшой код, и он, похоже, работал.
Учитывая следующие три определения классов:
class Person {
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
class State {
public string Abbreviation { get; set; }
}
class Address {
public string City { get; set; }
public State State { get; set; }
}
Следующий метод даст вам полный путь свойства
static string GetFullSortName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression) {
var memberNames = new List<string>();
var memberExpression = expression.Body as MemberExpression;
while (null != memberExpression) {
memberNames.Add(memberExpression.Member.Name);
memberExpression = memberExpression.Expression as MemberExpression;
}
memberNames.Reverse();
string fullName = string.Join(".", memberNames.ToArray());
return fullName;
}
Для двух вызовов:
fullName = GetFullSortName<Person, string>(p => p.FirstName);
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation);
Ответ 6
Источник ExpressionHelper из MVC находится здесь
https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/ExpressionHelper.cs
Просто возьмите этот класс - и вы избежите зависимости от MVC и получите специальные кромки, обработанные для вас.
Отказ от ответственности: не знаете, как работает лицензирование, просто беря класс вроде этого - но кажется довольно безобидным
Ответ 7
На основании этого и нескольких связанных вопросов/ответов здесь, здесь используется простой метод:
protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)
{
// http://stackoverflow.com/questions/2789504/get-the-property-as-a-string-from-an-expressionfunctmodel-tproperty
// http://stackoverflow.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct
// http://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct
MemberExpression expr;
if (prop.Body is MemberExpression)
// .Net interpreted this code trivially like t => t.Id
expr = (MemberExpression)prop.Body;
else
// .Net wrapped this code in Convert to reduce errors, meaning it t => Convert(t.Id) - get at the
// t.Id inside
expr = (MemberExpression)((UnaryExpression)prop.Body).Operand;
string name = expr.Member.Name;
return name;
}
Вы можете использовать его просто так:
string name = propertyNameFromExpression(t => t.Id); // returns "Id"
Этот метод, однако, делает меньше проверок ошибок, чем другие, размещенные здесь, - в основном это считается само собой разумеющимся, что он называется правильно, что не может быть безопасным допуском в вашем приложении.
Ответ 8
Код, на котором я работаю 100%, выглядит следующим образом, но я не понимаю, что он делает (несмотря на то, что я его модифицировал, чтобы обработать эти глубоко вложенные сценарии благодаря отладчику).
internal static string MemberWithoutInstance(this LambdaExpression expression)
{
var memberExpression = expression.ToMemberExpression();
if (memberExpression == null)
{
return null;
}
if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess)
{
var innerMemberExpression = (MemberExpression) memberExpression.Expression;
while (innerMemberExpression.Expression.NodeType == ExpressionType.MemberAccess)
{
innerMemberExpression = (MemberExpression) innerMemberExpression.Expression;
}
var parameterExpression = (ParameterExpression) innerMemberExpression.Expression;
// +1 accounts for the ".".
return memberExpression.ToString().Substring(parameterExpression.ToString().Length + 1);
}
return memberExpression.Member.Name;
}
internal static MemberExpression ToMemberExpression(this LambdaExpression expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
var unaryExpression = expression.Body as UnaryExpression;
if (unaryExpression != null)
{
memberExpression = unaryExpression.Operand as MemberExpression;
}
}
return memberExpression;
}
public static string PropertyToString<TModel, TProperty>(this Expression<Func<TModel, TProperty>> source)
{
return source.MemberWithoutInstance();
}
Это решение обрабатывает его, когда мое выражение имеет тип Expression<Func<TModel,object>>
, и я передаю все виды типов объектов для моих параметров. Когда я это делаю, мое выражение x => x.Age
превращается в x => Convert(x.Age)
, и здесь здесь разрываются другие решения. Однако я не понимаю, что в этом обрабатывает часть Convert
.: -/
Ответ 9
Перекрестная регистрация из Извлечение имени свойства из выражения лямбда
Как говорится в этом вопросе, скрытый ответ заключается в том, что если вы вызываете expression.ToString()
, это даст вам что-то вроде:
"o => o.ParentProperty.ChildProperty"
который вы можете просто подстроить с первого периода.
Основываясь на некоторых тестах LinqPad, производительность была сопоставимой.