System refleksji dostępny w .NET daje programiście duże możliwości operowanie strukturami klas podczas działania programu. Pobieranie informacji o poszczególnych elementach klasy wiążę się zazwyczaj z wywołaniem odpowiedniej metody i podania nazwy elementu klasy o którym informację chcielibyśmy uzyskać. Taki sposób operowania wewnętrzną strukturą programu jest mało wygodny i może powodować błędy podczas działania aplikacji.
Pewnie nie raz spotkaliście się z poniższym zapisem np. pisząc aplikację w ASP.NET MVC czy korzystając z frameworku Fluent NHibernate do definiowania mapowań dla NHibernate’a.
[csharp]
GetProperty(x => x.Name);
GetProperty(x => x.LastName);
GetProperty(x => x.Address.City);
[/csharp]
Wielu z was pewnie zastanawiało się jak zastosować taki mechanizm w pisanej bibliotece czy aplikacji. Z uwagi na to, że jakiś czas temu pisałem bibliotekę wykorzystującą taki sposób wskazywania elementów klasy postanowiłem, że opiszę pokrótce jak go zastosować. Główną zaletą stosowania takiego zapisu jest fakt, że dostajemy już na etapie kompilacji weryfikację poprawności takiego wyrażenia i automatyczne podpowiadanie składni na etapie jego tworzenia.
Żeby więcej nie przynudzać przejdę od razu do zaprezentowanie przykładu zastosowania wspomnianego mechanizmu. Zacznijmy od dwóch prostych klas których elementy będziemy wybierać:
[csharp]public enum Gender { Male, Female };
public class Person
{
public long Id { get; private set; }
public string Name { get; set; }
public string LastName { get; set; }
public string Profession { get; set; }
public Gender Gender { get; set; }
public DateTime BirthDate { get; set; }
public Address Address { get; set; }
}
[/csharp]
[csharp]public class Address
{
public string City { get; set; }
public string Street { get; set; }
public int Home { get; set; }
}
[/csharp]
Jak widzimy są to klasy reprezentujące odpowiednio osobę jak i adres. Aby móc pobierać informację o elementach klas musimy mieć narzędzie które nam to umożliwi. Do tego celu stworzymy prostą klasę generyczną która zwróci nam odpowiednie obiekty zawierające informację które chcemy uzyskać.
[csharp]
public class Info<T> where T: class
{
public PropertyInfo GetProperty(Expression<Func<T,object>> expression)
{
return ReflectionHelper.GetMemberExpression(expression.Body).Member as PropertyInfo;
}
public PropertyInfo[] GetAllPropertiesFromExpression(Expression<Func<T, object>> expression)
{
return ReflectionHelper
.GetPropertiesFromExpression(expression.Body);
}
}
[/csharp]
Jak widać powyżej klasa posiada dwie meody:
- GetProperty – zwracająca obiekt informacyjny o właściwości wskazanej przez wyrażenie lambda
- GetAllPropertiesFromExpression – metoda zwracająca wszystkie właściwości w całym wyrażeniu
Obie metody jako parametr przyjmują System.Linq.Expressions.Expression<Func<T, object>>. Fakt, że w wyrażeniu operujemy delegatem funkcji daje nam to możliwość zastosowania wyrażeń lambda do ich tworzenia dzięki czemu uzyskujemy prosty zapis i podpowiadanie składni przy ich pisaniu.
Aby pobrać interesujące nas informację nie korzystamy z całości wyrażenia, a jedynie z jego części po operatorze “=>”. Dostęp do tej części uzyskujemy poprzez właściwość Body. Obiekt klasy System.Linq.Expressions.Expression zwracany przez właściwość Body nie posiada elementów z których możemy bezpośrednio uzyskać informację o członkach klasy które nas interesują. System.Linq.Expressions.Expression jest bazową klasą wszystkich wyrażeń i musimy ją rzutować, w naszym przypadku na obiekty klasy MemberExpression. Metoda realizująca to zadanie znajduje się poniżej.
[csharp]
public static MemberExpression GetMemberExpression(Expression expression)
{
MemberExpression memberExpression = null;
if (expression.NodeType == ExpressionType.Convert)
{
UnaryExpression body = (UnaryExpression)expression;
memberExpression = body.Operand as MemberExpression;
}
else if (expression.NodeType == ExpressionType.MemberAccess)
{
memberExpression = expression as MemberExpression;
}
if (memberExpression == null)
{
throw new ArgumentException(
String.Format("Expression {0} is not MemberExpression", expression.NodeType));
}
return memberExpression;
}
[/csharp]
W metodzie tej zawarta jest również część odpowiedzialna za rzutowanie wyrażenia na UnaryExpression i dopiero po tym uzyskujemy z niego obiekt klasy MemberExpression. Taka operacja jest wymagana aby dodać obsługę obiektów ValueType. Bez zastosowania UnaryExpression nie moglibyśmy pobierać informacji o członkach klasy będących ValueType.
Metoda GetMemberExpression umożliwia pobranie tylko ostatniego elementu wyrażenia. Jeżeli byśmy chcieli pobrać wszystkie właściwości z wyrażenia składającego się z kilku elementów musimy pobierać te elementy pojedynczo zaczynając od ostatniego i cofając się do elementu będącego parametrem. Metodę realizująca do zadanie widzimy poniżej.
[csharp]
public static PropertyInfo[] GetPropertiesFromExpression(Expression expression)
{
List properties = new List();
MemberExpression memberExpression = null;
Expression ex = expression;
do
{
memberExpression = ReflectionHelper.GetMemberExpression(ex);
properties.Add((PropertyInfo)memberExpression.Member);
ex = memberExpression.Expression;
}
while (ex.NodeType != ExpressionType.Parameter);
properties.Reverse();
return properties.ToArray();
}
[/csharp]
Mając gotowe narzędzia możemy je wykorzystać w testowym programie który mógłby wyglądać mniej więcej tak:
[csharp]
Console.WriteLine("Person class:");
Info personInfo = new Info();
Console.WriteLine("Get \"Name\" Property: x => x.Name");
PropertyInfo personName = personInfo.GetProperty(x => x.Name);
Console.WriteLine("Type: {0}, Property: {1}",
personName.DeclaringType.Name, personName.Name);
Console.WriteLine();
Console.WriteLine("Get all properties from expression: x => x.Address.City.Length");
var properties = personInfo.GetAllPropertiesFromExpression(x => x.Address.City.Length);
foreach (var item in properties)
{
Console.WriteLine("Type: {0}, Property: {1}",
item.DeclaringType.Name, item.Name);
}
[/csharp]
Po wykonaniu powyższego kawałku kodu na ekranie powinniśmy zobaczyć następujący efekt:
Person class: Get "Name" Property: x => x.Name Type: Person, Property: Name Get all properties from expression: x => x.Address.City.Length Type: Person, Property: Address Type: Address, Property: City Type: String, Property: Length
Kod zawierający program testowy wraz z wszystkimi opisanymi klasami można pobrać tutaj:
[dm]2[/dm]