Silverlight – WCF – Simplifier les appels aux services WCF grâce aux Reactive Extensions

7 minutes read

Ceux ayant déjà développé des applications contenant de nombreux appels WCF depuis un client Silverlight ont pu remarquer combien il est fastidieux de gérer les références aux évènements *Completed des méthodes WCF. Le générateur de proxy de Silverlight créé un évènement ***Completed pour chaque méthode WCF ce qui conduit à une multiplication des event handler. Cette multiplication est problématique pour la gestion des désabonnements (et donc des fuites de mémoire) et pour le chaînage des appels (ne pas oublier qu’en Silverlight tout les appels à des services web sont asynchrones).

Grâce aux Reactive Extensions, nous pouvons cependant nous simplifier la tâche et chaîner les appels aux services de manière lisible (et donc maintenable facilement) et sans risques de fuites de mémoire.

Le contexte

Prenons des exemples et interrogeons le service WCF suivant :

[ServiceContract]
public interface IUserService
{
    [OperationContract]
    int Count();

    [OperationContract]
    IEnumerable<User> GetUsers(int from, int count);

    [OperationContract]
    User GetUser(int userId);
}

Dans notre projet Silverlight, après avoir ajouté une référence à notre service, nous obtenons la classe UserServiceClient dans le namespace UserService.

UserServiceReference

Premier appel avec les Reactive Extensions

Pour appeler la méthode GetUser de notre service nous utilisons la méthode GetUserAsync de notre proxy. Avec les Reactive Extensions le code est le suivant :

var client = new UserServiceClient();
IObservable<IEvent<GetUserCompletedEventArgs>> observable =
    Observable.FromEvent<GetUserCompletedEventArgs>(client, "GetUserCompleted")
    .Take(1);

IDisposable d = null;
d = observable.Subscribe(u =>
{
    User = u.EventArgs.Result;
    d.Dispose();
});

client.GetUserAsync(0);

Premièrement nous instancions donc notre service. Ensuite nous créons une collection observable qui va s’abonner à l’évènement GetUserCompleted (deuxième paramètre de Observable.FromEvent) renvoyant un élément de type GetUserCompletedEventArgs et qui ne va écouter cet évènement qu’une seule fois grâce à la méthode .Take(1)).

Nous créons ensuite un objet de type IDisposable et nous nous abonnons à la collection observable du définie précédemment. La méthode Subscribe prend en paramètre une Action qui sera executée à chaque élément renvoyé par la collection observable, ce qui correspond ici à un l’évènement GetUserCompleted. Nous effectuons notre action (ici mettre à jour la propriété utilisateur du ViewModel courant) et libérons la collection entrainant le désabonnement à GetUserCompleted en appellant d.Dispose().

Maintenant il ne reste plus qu’à appeler la méthode GetUserAsync pour que tout ce que nous avons défini précédement soit executé.

Certains diront, “Le code est encore plus gros et plus verbeux que si on fait un simple appel classique!” et ils auraient raison. En effet l’intérêt principal de cette méthode réside ailleurs.

Chaînage des appels

Grâce à cette méthode nous pouvons enchaîner les appels sans avoir à définir des champs dans notre ViewModel pour sauvegarder les états entre les appels etc..

Exemple :

Nous souhaitons récupérer la liste complète des utilisateurs. Notre contrat de service définit deux méthodes qui nous permettent de faire celà : Count() et GetUsers(int from, int count). Nous devons cependant préalablement appeller la méthode Count() afin d’utiliser son résultat en deuxième paramètre de GetUsers.

Si nous utilisions l’approche classique nous devrions nous abonner aux évènements CountCompleted et GetUsersCompleted et dans le gestionnaire de l’évènement CountCompleted appeler la méthode GetUsers. Imaginons un instant que la méthode Count soit utilisée aussi avant de lancer la suppression de tout les objets avec une méthode qui aurait la signature suivante Remove(int from, int count), nous serions obligé d’avoir un autre gestionnaire d’évènement pour l’évènement CountCompleted qui lui appellerai la méthode Remove. Sans compter qu’il faudrait gérer les abonnements et les désabonnements à tout ces évènements.

Tout ceci étant un travail fastidieux (et le développeur est fainéant c’est connu), et sujet aux erreurs et aux bugs (et le développeur n’aime pas les bugs car ils peuvent lui faire rater son dîner ou pire sa partie de Starcraft), le fait de pouvoir chaîner les appels est important.

Voici le code qui nous permet de parvenir à nos fins :

var obs1 = Observable.FromEvent<CountCompletedEventArgs>(client, "CountCompleted").Take(1);

IDisposable d1 = null;
d1 = obs1.Subscribe(r =>
{
    int count = r.EventArgs.Result;

    var obs2 =
        Observable.FromEvent<GetUsersCompletedEventArgs>(client, "GetUsersCompleted").Take(1);

    IDisposable d2 = null;
    d2 = obs2.Subscribe(r2 =>
    {
        User = r2.EventArgs.Result.LastOrDefault();
        d2.Dispose();
    });

    client.GetUsersAsync(0, count);

    d1.Dispose();
});

client.CountAsync();

Et là certains diraient “Bon ok on peux chaîner les appels mais le code est super gros et on répète plusieurs fois l’appel à Dispose, à Subscribe, à Observable.FromEvent etc…” et ils auraient encore raison!

Factorisation du code

Un des principes de base dans le développement est le DRY (don’t repeat yourself) ou factorisation de code dans notre langue de molière.

Je vais donc vous proposer une méthode qui va permettre de factoriser tout celà et d’obtenir un code lisible, chaînable, et gérant les abonnements désabonnements aux évènements qu’ils utilisent.

Nous allons tout d’abord créer une classe de méthodes d’extensions qui contiendra la plupart de la logique de factorisation :

public static class ServiceExtensions
{
    private const string InvalidActionMessage = "Call must be done on an asynchronous method.";

    private static string GetMethodName<T>(Expression<Action<T>> expression)
    {
        var methodCallExpression = expression.Body as MethodCallExpression;
        if (methodCallExpression != null)
            return methodCallExpression.Method.Name;
        return null;
    }

    private static IObservable<IEvent<TEventArgs>> GetObservable<T, TEventArgs>(this T source, Expression<Action<T>> expression)
        where TEventArgs : EventArgs
    {
        string methodName = GetMethodName(expression);

        if (methodName.EndsWith("Async") == false)
            throw new Exception(InvalidActionMessage);

        string eventName = methodName.Replace("Async", "Completed");

        IObservable<IEvent<TEventArgs>> observable =
            Observable.FromEvent<TEventArgs>(source, eventName).Take(1);

        return observable;
    }

    public static void Call<T>(this T source, Expression<Action<T>> expression)
    {
        IObservable<IEvent<EventArgs>> observable = GetObservable<T, EventArgs>(source, expression);

        IDisposable s = null;
        s = observable.Subscribe(u => s.Dispose());

        Action<T> action = expression.Compile();
        action.Invoke(source);
    }

    public static void Call<T>(this T source, Expression<Action<T>> expression, Action callback)
    {
        IObservable<IEvent<EventArgs>> observable = GetObservable<T, EventArgs>(source, expression);

        IDisposable s = null;
        s = observable.Subscribe(
            u =>
            {
                callback();
                s.Dispose();
            });

        Action<T> action = expression.Compile();
        action.Invoke(source);
    }

    public static void Call<T, TEventArgs>(this T source, Expression<Action<T>> expression, Action<TEventArgs> callback)
        where TEventArgs : EventArgs
    {
        IObservable<IEvent<TEventArgs>> observable = GetObservable<T, TEventArgs>(source, expression);

        IDisposable s = null;
        s = observable.Subscribe(
            u =>
            {
                callback(u.EventArgs);
                s.Dispose();
            });

        Action<T> action = expression.Compile();
        action.Invoke(source);
    }
}

J’aimerai attirer votre attention sur le fonctionnement général de ces méthodes. Prenons la dernière définie (la plus complète).

Le premier paramètre générique T correspond au type de notre service (ici ce sera UserServiceClient). Le deuxième paramètre générique TEventArgs correspond au type d’évènement lancé lors de l’ fin de l’appel asynchrone au service WCF.

”source” correspond à l’objet qui executera l’appel (notre proxy), “expression” expression décrivant l’action à executer (ici l’appel à notre méthode) et enfin callback est la méthode que nous executerons à la fin de l’appel asynchrone.

Vous pouvez déjà remarquer que le code est sembable à ce que vous avez pu voir dans la deuxième partie de cet article. La création de la collection observable a été factorisée dans une autre méthode nommée GetObservable et c’est ici qu’intervient un peu de “magie”.

Afin de mieux vous l’expliquer je vais recopier ci-dessous une version commentée de cette méthode.

/// <summary>
/// Génère la collection observable utilisée pour l'appel asynchrone au service WCF.
/// </summary>
/// <typeparam name="T">Type du service.</typeparam>
/// <typeparam name="TEventArgs">Type de l'eventargs renvoyé par l'évènement completed.</typeparam>
/// <param name="source">Instance du service.</param>
/// <param name="expression">Expression contenant l'appel au service.</param>
/// <returns>Collection observable utilisée pour l'appel.</returns>
private static IObservable<IEvent<TEventArgs>> GetObservable<T, TEventArgs>(this T source, Expression<Action<T>> expression)
    where TEventArgs : EventArgs
{
    // On recherche le nom de la méthode à appeler
    string methodName = GetMethodName(expression);

    // Toutes les méthodes de service (à ma connaissance) générée lors de l'ajout
    // d'une référence de service sont suffixées par Async pour montrer leurs
    // fonctionnement asynchrone.
    if (methodName.EndsWith("Async") == false)
        throw new Exception(InvalidActionMessage);

    // Il suffit de remplacer Async par Completed pour avoir le nom de l'évènement
    // Exemple : GetUserAsync => GetUserCompleted
    string eventName = methodName.Replace("Async", "Completed");

    // Création de la collection avec le nom de l'évènement déduit ci-dessus.
    IObservable<IEvent<TEventArgs>> observable =
        Observable.FromEvent<TEventArgs>(source, eventName).Take(1);

    return observable;
}

De cette manière nous pouvons nous passer de nommer explicitement l’évènement completed comme nous l’avions fait jusqu’à lors.

Pour la suite nous allons créer un décorateur pour les classes de services :

public class ServiceWrapper<TService>
{
    private readonly TService _service;

    public ServiceWrapper(TService service)
    {
        _service = service;
    }

    public void Call(Expression<Action<TService>> expression)
    {
        _service.Call(expression);
    }

    public void Call(Expression<Action<TService>> expression, Action callback)
    {
        _service.Call(expression, callback);
    }

    public void Call<TEventArgs>(Expression<Action<TService>> expression, Action<TEventArgs> callback)
        where TEventArgs : EventArgs
    {
        _service.Call(expression, callback);
    }
}

Ce décorateur reprend les méthodes que nous avons dans notre classe d’extensions mais nous permet de nous affranchir du premier paramètre générique.

Ensuite nous allons créer une factory de services (ici nous n’en avons qu’un seul mais c’est tout de même utile surtout si l’on met en place de l’IoC) dont la tâche sera de nous renvoyer instance du wrapper précédent.

public class ServiceClientFactory
{
    private ServiceWrapper<UserServiceClient> _userService;

    public ServiceWrapper<UserServiceClient> UserService
    {
        get
        {
            return _userService ?? (_userService = new ServiceWrapper<UserServiceClient>(new UserServiceClient()));
        }
    }
}

Grâce à tout ceci le code de notre appel chainé ressemble desormais à celà :

var factory = new ServiceClientFactory();
factory.UserService.Call<CountCompletedEventArgs>(
    s => s.CountAsync(),
    r => factory.UserService.Call<GetUsersCompletedEventArgs>(
        s => s.GetUsersAsync(0, r.Result),
        r2 => User = r2.Result.LastOrDefault()));

Un beau régime n’est-ce pas ?

Conclusion

Nous pouvons donc maintenant chaîner des appels sans se soucis de l’abonnement ou du désabonnement aux évènement de type ***completed, le tout avec beaucoup moins de code.

J’espère que ces bouts de code pourront vous aider autant qu’ils m’aident depuis que je les utilise.

Voici le lien vers le code source de l’application pour ceux que celà interesse => mon skydrive.

Updated:

Leave a Comment