ICommandHandler / IQueryHandler avec async / wait

EDITH says (tl;dr)

je suis allé avec une variante de la solution suggérée; garder tout ICommandHandleret IQueryHandler s potentiellement aynchrone et renvoie une tâche résolue dans les cas synchrones. Encore, je ne veux pas utiliser Task.FromResult(...) tous les coins de la place, j'ai donc défini une méthode d'extension pour plus de commodité:

public static class TaskExtensions
{
    public static Task<TResult> AsTaskResult<TResult>(this TResult result)
    {
        // Or TaskEx.FromResult if you're targeting .NET4.0 
        // with the Microsoft.BCL.Async package
        return Task.FromResult(result); 
    }
}

// Usage in code ...
using TaskExtensions;
class MySynchronousQueryHandler : IQueryHandler<MyQuery, bool>
{
    public Task<bool> Handle(MyQuery query)
    {
        return true.AsTaskResult();
    }
}

class MyAsynchronousQueryHandler : IQueryHandler<MyQuery, bool>
{
    public async Task<bool> Handle(MyQuery query)
    {
        return await this.callAWebserviceToReturnTheResult();
    }
}

il est dommage que C# ne soit pas Haskell ... encore 8 -). Ça sent vraiment comme une application de Flèches. De toute façon, espérons que cette aide quiconque. Maintenant à ma question originale: -)

Introduction

Bonjour à tous!

pour un projet, je suis en train de concevoir une architecture d'application en C# (.NET4.5, C # 5.0, ASP.NET MVC4). Avec cette question, j'espère avoir quelques opinions sur certaines questions que je suis tombé sur essayer d'intégrer async/await. Remarque: c'est assez long :-)

ma structure de solution ressemble à ceci:

  • MyCompany.Contract (Commandes/Requêtes et interfaces communes)
  • MyCompany.MyProject (Contient la logique métier et de commande/de requête gestionnaires)
  • MyCompany.MyProject.Web (MVC web frontend)

j'ai lu sur l'architecture maintenable et commande-requête-séparation et j'ai trouvé ces messages très utiles:

jusqu'à présent, j'ai la tête autour de l' ICommandHandler/IQueryHandler concepts et l'injection de dépendance (je suis en utilisant SimpleInjector - c'est vraiment simple).

L'Approche Donnée

L'approche des articles ci-dessus suggère d'utiliser POCOs que les commandes/requêtes et décrit les répartiteurs de ces implémentations de la fonction suivante interfaces:

interface IQueryHandler<TQuery, TResult>
{
    TResult Handle(TQuery query);
}

interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

dans un MVC Contrôleur que vous souhaitez utiliser comme suit:

class AuthenticateCommand
{
    // The token to use for authentication
    public string Token { get; set; }
    public string SomeResultingSessionId { get; set; }
}

class AuthenticateController : Controller
{
    private readonly ICommandHandler<AuthenticateCommand> authenticateUser;

    public AuthenticateController(ICommandHandler<AuthenticateCommand> authenticateUser) 
    {
        // Injected via DI container
        this.authenticateUser = authenticateUser;
    }

    public ActionResult Index(string externalToken)
    {
        var command = new AuthenticateCommand 
        { 
            Token = externalToken 
        };
        this.authenticateUser.Handle(command);

        var sessionId = command.SomeResultingSessionId;
        // Do some fancy thing with our new found knowledge
    }
}

Certains de mes observations à ce sujet:

  1. pure CQS seules les requêtes devraient retourner des valeurs alors que les commandes devraient être, et bien seulement des commandes. En réalité, il est plus pratique pour les commandes de retourner des valeurs au lieu d'émettre la commande et de faire ensuite une requête pour la chose que la commande aurait dû retourner en premier lieu (par exemple ids de base de données ou similaire). C'est pourquoi la l'auteur a suggéré mettre une valeur de retour dans la commande POCO.
  2. il n'est pas très évident ce qui est retourné d'une commande, en fait il regarde comme la commande est un feu et oublier type de chose jusqu'à ce que vous rencontriez éventuellement la propriété de résultat bizarre étant accédé après le handler a couru plus la commande sait maintenant à propos de son résultat
  3. Les gestionnaires d' pour être synchrone pour que cela fonctionne - requêtes ainsi que les commandes. Comme il s'avère qu'avec C#5.0 vous pouvez injecter async/await powered handlers avec l'aide de votre conteneur DI préféré, mais le compilateur ne le sait pas au moment de la compilation, donc le handler MVC va échouer misérablement avec une exception vous disant que la méthode est revenue avant que toutes les tâches asynchrones aient fini d'exécuter.

bien sûr, vous pouvez marquer le handler MVC comme async et c'est sur quoi porte cette question.

Commandes Retournant Des Valeurs

je réflexion sur l'approche donnée et modifications apportées aux interfaces pour régler les problèmes 1. et 2. j'ai ajouté un ICommandHandler qui a un type de résultat explicite - tout comme le IQueryHandler. Cela viole toujours CQS mais au moins il est évident que ces commandes retournent une sorte de valeur avec l'avantage supplémentaire de ne pas avoir à encombrer l'objet de commande avec une propriété de résultat:

interface ICommandHandler<TCommand, TResult>
{
    TResult Handle(TCommand command);
}

naturellement, on pourrait faire valoir que lorsque vous avez la même interface pour les commandes et QUESTIONS Pourquoi s'embêter? Mais je pense que ça vaut la peine de les nommer autrement - ça a juste l'air plus propre à mes yeux.

Ma Solution Préliminaire

puis j'ai beaucoup réfléchi au 3e numéro à portée de main ... certains de mes gestionnaires de commandes/requêtes doivent être asynchrones (par exemple, émettre un WebRequest vers un autre service web pour authentification) d'autres non. J'ai donc pensé qu'il serait préférable de concevoir mes handlers à partir de la base pour async/await - qui, bien sûr, fait des bulles jusqu'aux manipulateurs MVC même pour les gestionnaires qui sont en fait de façon synchrone:

interface IQueryHandler<TQuery, TResult>
{
    Task<TResult> Handle(TQuery query);
}

interface ICommandHandler<TCommand>
{
    Task Handle(TCommand command);
}

interface ICommandHandler<TCommand, TResult>
{
    Task<TResult> Handle(TCommand command);
}

class AuthenticateCommand
{
    // The token to use for authentication
    public string Token { get; set; }

    // No more return properties ...
}

AuthenticateController:

class AuthenticateController : Controller
{
    private readonly ICommandHandler<AuthenticateCommand, string> authenticateUser;

    public AuthenticateController(ICommandHandler<AuthenticateCommand, 
        string> authenticateUser) 
    {
        // Injected via DI container
        this.authenticateUser = authenticateUser;
    }

    public async Task<ActionResult> Index(string externalToken)
    {
        var command = new AuthenticateCommand 
        { 
            Token = externalToken 
        };
        // It's pretty obvious that the command handler returns something
        var sessionId = await this.authenticateUser.Handle(command);

        // Do some fancy thing with our new found knowledge
    }
}

bien que cela résolve mes problèmes-valeurs de retour évidentes, tous les gestionnaires peuvent être async - cela me fait mal au cerveau de mettre async sur un truc qui n'est pas asynchrone juste parce que. Il y a plusieurs inconvénients que je vois avec ceci:

  • les interfaces de handler ne sont pas aussi soignées que je le voulais - les Task<...> les choses à mes yeux sont très verbeuses et à première vue dissimuler le fait que je ne veux retourner quelque chose à partir d'une requête/commande
  • Le compilateur vous avertit de ne pas avoir un approprié await dans les implémentations de handler synchrones (je veux pouvoir compiler mes ReleaseWarnings as Errors) - vous pouvez l'écraser avec un pragma ... ouais. .. bien. ..
  • j'ai pu omettre l' async mot-clé dans ces cas pour rendre le compilateur heureux mais pour mettre en œuvre l'interface de handler vous devriez retourner certains sorte de Task explicitement - c'est assez laid
  • je pourrais fournir des versions synchrones et asynchrones des interfaces handler (ou les mettre toutes dans une interface gonflant l'implémentation) mais je crois comprendre que, idéalement, le consommateur d'un handler ne devrait pas être conscient du fait qu'un handler commande/requête est synchrone ou asynchrone car c'est une préoccupation transversale. Que faire si j'ai besoin de faire un autrefois commande synchrone asynchrone? Je dois changer tous les consommateurs de la handler risque de briser la sémantique en parcourant le code.
  • d'autre part, l' éventuellement-async-gestionnaires-approche serait à même de me donner la possibilité de changer de synchroniser les maîtres de async en les décorant avec l'aide de mon DI conteneur

pour l'instant je ne vois pas solution à cette question ... Je suis à une perte.

Quelqu'un ayant un problème similaire et une solution élégante à laquelle je n'ai pas pensé?

14
demandé sur Dave Schweisguth 2014-01-08 04:41:36

4 réponses

Async et wait ne se mélangent pas parfaitement avec l'OOP traditionnel. J'ai une série de blog sur le sujet, vous pouvez trouver le post sur async interfaces utile en particulier (bien que je ne couvre rien que vous n'avez pas encore découvert).

Les problèmes de conception autour de async sont très similaires à ceux autour de IDisposable; c'est une modification de rupture d'ajouter IDisposable à une interface, donc vous devez savoir si une implémentation possible peut jamais être jetable (détail de mise en œuvre). Un problème parallèle existe avec async; vous avez besoin de savoir si une éventuelle mise en œuvre peut jamais être asynchrone (détail de mise en oeuvre).

Pour ces raisons, j'ai vue Task-retourner les méthodes sur une interface comme des méthodes "éventuellement asynchrones", tout comme une interface héritant de IDisposable signifie qu'il " possède peut-être des ressources."

La meilleure méthode que je connaisse est:

  • Définir toutes les méthodes qui sont éventuellement-asynchrones avec une signature asynchrone (retour Task/Task<T>).
  • Retour Task.FromResult(...) pour les implémentations synchrones. C'est plus approprié qu'un async sans await.

cette approche est presque exactement ce que vous faites déjà. Il existe peut-être une solution plus idéale pour un langage purement fonctionnel, mais je n'en vois pas pour C#.

15
répondu Stephen Cleary 2014-01-08 02:05:50

j'ai créé un projet juste pour cela - j'ai fini par ne pas séparer les commandes et les requêtes, au lieu d'utiliser request / response et pub / sub -https://github.com/jbogard/MediatR

public interface IMediator
{
    TResponse Send<TResponse>(IRequest<TResponse> request);
    Task<TResponse> SendAsync<TResponse>(IAsyncRequest<TResponse> request);
    void Publish<TNotification>(TNotification notification) where TNotification : INotification;
    Task PublishAsync<TNotification>(TNotification notification) where TNotification : IAsyncNotification;
}

pour le cas où les commandes ne renvoient pas les résultats, j'ai utilisé une classe de base qui renvoie un type de vide (unité pour les personnes fonctionnelles). Cela m'a permis d'avoir une interface uniforme pour envoyer des messages qui ont des réponses, avec une réponse nulle étant une valeur de retour explicite.

comme quelqu'un qui expose une commande, vous optez explicitement pour être asynchrone dans votre définition de la requête, plutôt que de forcer tout le monde à être asynchrone.

10
répondu Jimmy Bogard 2014-05-08 13:03:22

Vous l'état:

la consommation d'un gestionnaire ne devrait pas être conscient du fait qu'une le gestionnaire de commandes / requêtes est sync ou async puisqu'il s'agit d'une coupe transversale préoccupation

Stephen a clairement déjà touché un peu à cela, mais async n'est pas une préoccupation transversale (ou du moins pas la façon dont elle est implémentée dans .NET). Async est un problème d'architecture puisque vous devez décider à l'avance de l'utiliser ou non, et il a toutes les chances de votre code d'application. Il change vos interfaces et il est donc impossible de "se faufiler" ceci dedans, sans l'application pour le savoir.

bien que .NET ait rendu async plus facile, comme vous l'avez dit, ça fait encore mal aux yeux et à l'esprit. Peut-être qu'il a juste besoin de formation mentale, mais je me demande vraiment si cela vaut la peine d'aller async pour la plupart des applications.

dans les deux cas, éviter d'avoir deux interfaces pour les gestionnaires de commandes. Vous devez en choisir une, parce qu'avoir deux interfaces séparées vous forcer à dupliquer tous vos décorateurs que vous voulez appliquer à eux et dupliquer votre configuration de DI. Donc, l'un ou l'autre ont une interface qui retourne Task et utilise les propriétés de sortie, ou aller avec Task<TResut> et retourner une sorte de Void tapez s'il n'y a pas de type de retour.

Comme vous pouvez l'imaginer (les articles que vous pointez à la mine) ma préférence personnelle est d'avoir un void Handle ou Task Handle méthode, car avec les commandes, l'accent est mis non pas sur la valeur de retour et quand avoir un la valeur de retour, vous aurez une double structure d'interface comme les requêtes:

public interface ICommand<TResult> { }

public interface ICommandHandler<TCommand, TResult> 
    where TCommand : ICommand<TResult> 
{ 
    Task<TResult> Handle(TCommand command);
}

Sans ICommand<TResult> interface et la contrainte de type générique, vous manquerez le support de compilation time. C'est quelque chose que j'ai expliqué dans en attendant... sur la requête côté de mon architecture

9
répondu Steven 2014-05-09 08:30:29

ce n'est pas vraiment une réponse, mais pour ce que ça vaut, j'en suis arrivé aux mêmes conclusions, et à une mise en œuvre très similaire.

Mon ICommandHandler<T> et IQueryHandler<T> retour Task et Task<T> respectivement. Dans le cas d'une synchrone de mise en œuvre-je utiliser Task.FromResult(...). J'ai aussi eu quelques décorateurs *handler en place (comme pour la journalisation) et comme vous pouvez l'imaginer, ceux-ci devaient aussi être changés.

pour l'instant, j'ai décidé de faire en sorte que "tout" soit potentiellement l'habitude d'utiliser await en conjonction avec mon dispatcher (trouve handler dans ninject kernel et calls handle dessus).

je suis allé async tout le chemin, aussi dans mes contrôleurs webapi/mvc, à quelques exceptions près. Dans ces rares cas, j'utilise Continuewith(...) et Wait() pour envelopper les choses dans une méthode synchrone.

une autre frustration liée que j'ai est que MR recommande de nommer les méthodes avec le suffixe *Async dans le cas où thay sont (duh) async. Mais c'est une mise en œuvre de la décision i (pour l'instant) a décidé de coller avec Handle(...) plutôt que HandleAsync(...).

ce n'est certainement pas un résultat satisfaisant et je cherche aussi une meilleure solution.

4
répondu Geoffrey Braaf 2014-01-23 15:41:55