Lire et modifier une propriété avec les Expression Trees (c#)

Voici l’objectif recherché :

var sampleObj = new MyObject(); 
sampleObj.Update(t => t.Foo, "bar");

Les Expression Trees sont à la base du langage Linq. Certes, le coût en performances n’est pas négligeable (réflexion et compilation de code dynamique). Mais couplés aux expressions lambda, ils sont un moyen astucieux pour faciliter le développement sur certains frameworks où le code est très répétitif. Dans mon cas, je les utilise beaucoup avec le sdk de Dynamics CRM (ceux qui connaissent feront vite le lien).

Voici un exemple complet :

private void Main()
{
    var sampleObj = new MyObject();
    sampleObj.Count = 10;
    
    Console.WriteLine(
        "state: Foo='{0}' ; Count={1}",
        sampleObj.Foo, sampleObj.Count);

    if (sampleObj.Update(t => t.Foo, "bar"))
        Console.WriteLine("Foo property updated.");
    if (sampleObj.Update(t => t.Count, 10))
        Console.WriteLine("Count property updated.");

    Console.WriteLine(
        "state: Foo='{0}' ; Count={1}",
        sampleObj.Foo, sampleObj.Count);
}

public static class ExtensionMethods
{
    public static bool Update<T, TValue>(this T model, Expression<Func<T, TValue>> propertySelector, TValue newValue) where T : MyObject
    {
        if (model == null)
            throw new ArgumentNullException("model");
        var memberExpression = propertySelector.Body as MemberExpression;
        if (memberExpression == null)
            throw new ArgumentException("propertySelector");

        TValue propertyValue = propertySelector.Compile()(model);

        if (!Convert.Equals(propertyValue, newValue))
        {
            var modelType = typeof(T);
            var propertyInfo = modelType.GetProperty(memberExpression.Member.Name);
            propertyInfo.SetValue(model, newValue);
            return true;
        }
        return false;
    }
}

public class MyObject
{
    public string Foo { get; set; }

    public int Count { get; set; }
}

Cet exemple est réduit à son minimum. Enrichi, il devient simple de modifier de façon conditionnelle les propriétés d’un objet et d’émettre une requête de mise à jour d’un service seulement si l’objet a été modifié.

Par exemple, pour du code fluent (chaînage de méthodes), on peut modifier légèrement la méthode Update de la façon suivante :

private void Main()
{
    var sampleObj = new MyObject();
    sampleObj.Count = 10;
    Console.WriteLine(
        "initial state: Foo='{0}' ; Count={1}",
        sampleObj.Foo, sampleObj.Count);

    int updateCount = 0;
    sampleObj
            .Update(t => t.Foo, "bar", ref updateCount)
            .Update(t => t.Count, 10, ref updateCount);

    if (updateCount != 0)
        Console.WriteLine(
          "state changed: Foo='{0}' ; Count={1}",
          sampleObj.Foo, sampleObj.Count);
}

public static T Update<T, TValue>(this T model, Expression<Func<T, TValue>> propertySelector, TValue newValue, ref int updateCount) where T : MyObject
{
	if (model == null)
	    throw new ArgumentNullException("model");
	var memberExpression = propertySelector.Body as MemberExpression;
	if (memberExpression == null)
	    throw new ArgumentException("propertySelector");

	TValue propertyValue = propertySelector.Compile()(model);

	if (!Convert.Equals(propertyValue, newValue))
	{
	    Type modelType = typeof(T);
	    PropertyInfo propertyInfo = modelType.GetProperty(memberExpression.Member.Name);
	    propertyInfo.SetValue(model, newValue);
	    updateCount++;
	}
	return model;
}

Attention aux performances: la compilation d’expressions est très pénalisante

Les deux exemples ci-dessus peuvent être améliorés (gains de l’ordre de 10 à 15x sur ma machine). La méthode MemberExpression.Compile() est particulièrement lente et superflue dans le scénario actuel. On l’utilise pour obtenir la valeur de la propriété décrite par l’expression lambda.

J’ai utilisé MemberExpression.Compile() dans un but illustratif pour cet article. La compilation d’expression est intéressante si, au moment d’écrire le code, on ne connait pas précisément la nature de la valeur obtenue. Si on lit une propriété, on sait que l’on peut utiliser directement PropertyInfo et c’est beaucoup plus efficace :

public static T Update<T, TValue>(this T model, Expression<Func<T, TValue>> propertySelector, TValue newValue, ref int updateCount) where T : MyObject
{
    if (model == null)
        throw new ArgumentNullException("model");
    var memberExpression = propertySelector.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("propertySelector");

    Type modelType = typeof(T);
    PropertyInfo propertyInfo = modelType.GetProperty(memberExpression.Member.Name);

    if (!Convert.Equals(propertyInfo.GetValue(model), newValue))
    {
        propertyInfo.SetValue(model, newValue);
        updateCount++;
    }
    return model;
}

Les exemples donnés sont très simples. Ce type de code devient rapidement difficile à lire, aussi je conseille un outil comme LinqPad. C’est payant, mais très pratique pour ce type d’exercice.