sintaxis - linq lambda c#
Obtener la propiedad, como una cadena, de una expresión<Func<TModel, TProperty>> (9)
Aquí está el truco: cualquier expresión de esta forma ...
obj => obj.A.B.C // etc.
... es realmente solo un grupo de objetos MemberExpression
anidados.
Primero tienes:
MemberExpression: obj.A.B.C
Expression: obj.A.B // MemberExpression
Member: C
Evaluar la Expression
anterior como MemberExpression
te da:
MemberExpression: obj.A.B
Expression: obj.A // MemberExpression
Member: B
Finalmente, encima de eso (en la "parte superior") tienes:
MemberExpression: obj.A
Expression: obj // note: not a MemberExpression
Member: A
Por lo tanto, parece claro que la forma de abordar este problema es mediante la comprobación de la propiedad Expression
de un MemberExpression
hasta el momento en que ya no es un MemberExpression
.
ACTUALIZACIÓN : parece que hay un giro adicional en su problema. Puede ser que tengas un lambda que se parece a un Func<T, int>
...
p => p.Age
... pero en realidad es un Func<T, object>
; en este caso, el compilador convertirá la expresión anterior en:
p => Convert(p.Age)
Ajustarse a este problema en realidad no es tan difícil como podría parecer. Eche un vistazo a mi código actualizado para una forma de tratarlo. Tenga en cuenta que al abstraer el código para obtener MemberExpression
en su propio método ( TryFindMemberExpression
), este enfoque mantiene el método GetFullPropertyName
bastante limpio y le permite agregar controles adicionales en el futuro si, tal vez, se encuentra frente a un nuevo escenario que no habías contabilizado originalmente, sin tener que pasar por demasiado código.
Para ilustrar: este código funcionó para mí.
// 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
);
}
Uso:
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));
Salida:
FirstName
Address.State.Abbreviation
Age
Utilizo algunas expresiones fuertemente tipadas que se serializan para permitir que mi código de UI tenga una clasificación fuerte y expresiones de búsqueda. Estos son de tipo Expression<Func<TModel,TProperty>>
y se utilizan como tales: SortOption.Field = (p => p.FirstName);
. He conseguido que esto funcione perfectamente para este caso simple.
El código que estoy usando para analizar la propiedad "FirstName" está reutilizando algunas funciones existentes en un producto de terceros que usamos y funciona muy bien, hasta que comenzamos a trabajar con propiedades profundamente anidadas ( SortOption.Field = (p => p.Address.State.Abbreviation);
). Este código tiene algunas suposiciones muy diferentes sobre la necesidad de admitir propiedades profundamente anidadas.
En cuanto a lo que hace este código, realmente no lo entiendo y en lugar de cambiar ese código, pensé que debería escribir desde cero esta funcionalidad. Sin embargo, no sé de una buena manera de hacer esto. Sospecho que podemos hacer algo mejor que hacer un ToString () y realizar un análisis sintáctico de cadenas. Entonces, ¿cuál es una buena manera de hacer esto para manejar los casos triviales y profundamente anidados?
Requisitos:
- Dada la expresión
p => p.FirstName
, necesito una cadena de"FirstName"
. - Dada la expresión
p => p.Address.State.Abbreviation
, necesito una cadena de"Address.State.Abbreviation"
Si bien no es importante para una respuesta a mi pregunta, sospecho que mi código de serialización / deserialización podría ser útil para otra persona que encuentre esta pregunta en el futuro, por lo que es más abajo. De nuevo, este código no es importante para la pregunta, solo pensé que podría ayudar a alguien. Tenga en cuenta que DynamicExpression.ParseLambda
proviene de las cosas de Dynamic LINQ y de lo que se trata esta pregunta es 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''s 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
}
Aquí hay un método que le permite obtener la representación de cadena, incluso cuando tiene propiedades anidadas:
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;
}
}
Si todavía está en .NET 3.5, necesita pegar un ToArray()
después de la llamada a Reverse()
, porque la sobrecarga de String.Join
que toma un IEnumerable
se agregó primero en .NET 4.
El código que tengo trabajando al 100% ahora es el siguiente, pero realmente no entiendo lo que está haciendo (a pesar de que lo modifiqué para hacerlo manejar estos escenarios profundamente anidados gracias al depurador).
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();
}
Esta solución maneja cuando mi expresión es de tipo Expression<Func<TModel,object>>
y paso todo tipo de tipos de objetos para mis parámetros. Cuando hago esto, mi expresión x => x.Age
se convierte en x => Convert(x.Age)
y eso rompe las otras soluciones aquí. Sin embargo, no entiendo qué maneja esto la parte Convert
. : - /
En función de esta y varias preguntas y respuestas relacionadas aquí, este es el método simple que estoy usando:
protected string propertyNameFromExpression<T>(Expression<Func<T, object>> prop)
{
// http://.com/questions/2789504/get-the-property-as-a-string-from-an-expressionfunctmodel-tproperty
// http://.com/questions/767733/converting-a-net-funct-to-a-net-expressionfunct
// http://.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''s t => Convert(t.Id) - get at the
// t.Id inside
expr = (MemberExpression)((UnaryExpression)prop.Body).Operand;
string name = expr.Member.Name;
return name;
}
Puedes usarlo simplemente como:
string name = propertyNameFromExpression(t => t.Id); // returns "Id"
Sin embargo, este método realiza menos comprobaciones de errores que otras publicaciones aquí; básicamente da por supuesto que se llama correctamente, lo que puede no ser una suposición segura en su aplicación.
Escribí un pequeño código para esto, y pareció funcionar.
Dadas las siguientes tres definiciones de clase:
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; }
}
El siguiente método le dará la ruta completa de la propiedad
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;
}
Para las dos llamadas:
fullName = GetFullSortName<Person, string>(p => p.FirstName);
fullName = GetFullSortName<Person, string>(p => p.Address.State.Abbreviation);
ExpressionHelper fuente de MVC está aquí
github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/…
Simplemente tome esta clase, y evitará tomar dependencia de MVC y obtener casos especiales para usted.
Descargo de responsabilidad: No estoy seguro de cómo funciona la licencia simplemente tomando una clase como esta, pero parece bastante inocua
Otro enfoque simple es usar el método System.Web.Mvc.ExpressionHelper.GetExpressionText. En mi próximo golpe escribiré más en detalle. Eche un vistazo a http://carrarini.blogspot.com/ .
Para "Nombre" de p => p.FirstName
Expression<Func<TModel, TProperty>> expression; //your given expression
string fieldName = ((MemberExpression)expression.Body).Member.Name; //watch out for runtime casting errors
Sugeriré que revises el código ASP.NET MVC 2 (de aspnet.codeplex.com) ya que tiene una API similar para helpers Html ... Html.TextBoxFor (p => p.FirstName) etc.
Publicación cruzada desde el nombre de recuperación de la propiedad de la expresión lambda
Como se hizo alusión a la pregunta, la respuesta furtiva es que si llama a expression.ToString()
, le dará algo como:
"o => o.ParentProperty.ChildProperty"
cuál puede entonces substring del primer período.
Basado en algunas pruebas de LinqPad , el rendimiento fue comparable.