CQRS Event Sourcing: Valider le nom d'utilisateur de l'unicité
Prenons un simple "Inscription en Compte", par exemple, en voici le déroulement:
- Visite de l'utilisateur sur site
- Cliquez sur "s'Inscrire" et remplissez le formulaire, cliquez sur le bouton "Enregistrer"
- MVC Contrôleur: Valider le nom d'utilisateur de l'unicité par la lecture de ReadModel
- RegisterCommand: Valider le nom d'utilisateur de l'unicité de nouveau (là est la question)
Bien sûr, nous pouvons valider le nom d'utilisateur de l'unicité par la lecture de ReadModel MVC contrôleur pour améliorer les performances et l'expérience utilisateur. Cependant, nous avons encore besoin de valider l'unicité de nouveau dans RegisterCommand, et, évidemment, nous ne devrions PAS accès ReadModel dans les Commandes.
Si nous n'utilisons pas d'Event Sourcing, nous pouvons interroger le modèle de domaine, donc ce n'est pas un problème. Mais si nous sommes à l'aide d'Event Sourcing, nous ne sommes pas en mesure d'interroger modèle de domaine, de sorte comment peut-on valider le nom d'utilisateur de l'unicité dans RegisterCommand?
Avis: classe Utilisateur dispose d'un Identifiant de propriété, et le nom d'utilisateur n'est pas la propriété de la clé de l'Utilisateur de la classe. Nous ne pouvons obtenir le domaine de l'objet par l'Id lors de l'utilisation d'event sourcing.
BTW: Dans l'obligation, si le nom d'utilisateur est déjà pris, le site doit afficher un message d'erreur "Désolé, le nom d'utilisateur XXX n'est pas disponible" pour le visiteur. Il n'est pas acceptable pour afficher un message, dire, "Nous sommes la création de votre compte, veuillez patienter, nous allons envoyer le résultat d'inscription par e-Mail plus tard", pour le visiteur.
Des idées? Merci beaucoup!
[Mise à JOUR]
Un exemple plus complexe:
Exigence:
Lors de la passation de commande, le système doit vérifier que le client de la commande de l'histoire, si il est un précieux client (si le client a placé à moins de 10 commandes par mois dans la dernière année, il a de la valeur), nous faisons 10% de rabais à l'ordre.
Mise en œuvre:
Nous créons PlaceOrderCommand, et dans la commande, nous avons besoin à la requête de la commande histoire de voir si le client est précieux. Mais comment pouvons-nous faire? Nous ne devrions pas accès ReadModel dans la commande! Comme Mikael dit, nous pouvons utiliser les commandes de compensation dans l'enregistrement d'un compte, par exemple, mais si nous utilisons également que, dans cet exemple de commande, il serait trop complexe, et que le code pourrait être trop difficile à maintenir.
Vous devez vous connecter pour publier un commentaire.
Si vous validez le nom d'utilisateur en utilisant le modèle de lire avant de vous envoyer la commande, nous parlons d'une condition de concurrence fenêtre de quelques centaines de millisecondes où une véritable course condition peut se produire, ce qui dans mon système n'est pas géré. Il est tout simplement trop peu de chances de se produire par rapport à le coût de traiter avec elle.
Toutefois, si vous sentez que vous devez gérer cela pour une raison quelconque ou si vous vous sentez vous voulez savoir comment maîtriser un tel cas, en voici une:
Vous ne devriez pas accéder à la lecture modèle à partir du gestionnaire de commande, ni le domaine lors de l'utilisation d'event sourcing. Cependant, ce que vous pourriez faire est d'utiliser un service de domaine qui serait à l'écoute de la UserRegistered événement dans lequel vous avez accès à la lecture modèle nouveau et vérifier si le nom d'utilisateur n'est toujours pas d'un doublon. Bien sûr, vous devez utiliser le UserGuid ici, ainsi que votre lecture modèle ait été mis à jour avec l'utilisateur que vous venez de créer. Si il y a un doublon trouvé, vous avez la possibilité d'envoyer des commandes de compensation telles que le changement de nom d'utilisateur et d'avertir l'utilisateur que le nom d'utilisateur a été prise.
C'est une approche du problème.
Comme vous pouvez probablement voir, il n'est pas possible de le faire en mode synchrone demande-réponse de la manière. Pour le résoudre, nous sommes SignalR de mettre à jour l'INTERFACE utilisateur chaque fois qu'il ya quelque chose que nous voulons pousser le client (si ils sont toujours connectés, qui est). Ce que nous faisons est que nous laissons sur le web client de souscrire à des événements qui contiennent de l'information qui est utile pour le client de voir immédiatement.
Mise à jour
Pour les cas plus complexes:
Je dirais que la commande est moins complexe, puisque vous pouvez utiliser le modèle de lire pour savoir si le client est précieux avant de vous envoyer la commande. En fait, vous pouvez lancer une requête que lorsque vous chargez le formulaire de commande puisque vous voulez probablement pour montrer au client qu'ils vont obtenir le rabais de 10% avant de passer la commande. Il suffit d'ajouter une réduction de prix à l'
PlaceOrderCommand
et peut-être une raison pour l'escompte, de sorte que vous pouvez suivre pourquoi vous êtes la coupe de profits.Mais là encore, si vous avez vraiment besoin de calculer la réduction après la commande a été des lieux, pour quelque raison, encore une fois utiliser un service de domaine qui serait à l'écoute de
OrderPlacedEvent
et la "compensation" de commande dans ce cas serait probablement unDiscountOrderCommand
ou quelque chose. Cette commande aurait une incidence sur l'Ordre d'Agrégation de la racine et de l'information pourrait être diffusée à votre lecture des modèles.Pour le double nom d'utilisateur de cas:
Vous pouvez envoyer un
ChangeUsernameCommand
que la compensation de commande à partir du domaine de service. Ou même quelque chose de plus spécifique, qui permettrait de décrire la raison pour laquelle le nom d'utilisateur changé, ce qui pourrait également entraîner la création d'un événement que le client web peut s'abonner à de sorte que vous pouvez permettre à l'utilisateur de voir que le nom d'utilisateur était un doublon.Dans le service de domaine de contexte, je dirais que vous avez aussi la possibilité d'utiliser d'autres moyens pour signaler à l'utilisateur, telles que l'envoi d'un e-mail qui pourrait être utile puisque vous ne pouvez pas savoir si l'utilisateur est toujours connecté. Peut-être que la fonctionnalité de notification pourrait être initiée par le même événement que le client web est vous abonnant à.
Quand il s'agit de SignalR, j'utilise un SignalR Hub que les utilisateurs se connecte à quand ils chargent une certaine forme. J'utilise le SignalR Groupe fonctionnalité qui me permet de créer un groupe dont j'ai le nom de la valeur de l'Guid-je envoyer la commande. Cela pourrait être le userGuid dans votre cas. Puis j'ai Eventhandler qui s'abonnent à des événements qui pourraient être utiles pour le client et quand un événement arrive, je peux appeler une fonction javascript sur tous les clients dans le SignalR Groupe (qui dans ce cas serait le seul client de la création de la double nom d'utilisateur dans votre cas). Je sais que cela semble complexe, mais il est vraiment pas. J'avais tous mis en place dans l'après-midi. Il y a de grands docs et des exemples sur la SignalR page Github.
Je pense que vous êtes pas encore eu le changement de mentalité à la cohérence des résultats et de la nature de l'event sourcing. J'ai eu le même problème. Plus précisément, j'ai refusé d'accepter que vous devez faire confiance à des commandes de la part du client, à l'aide de votre exemple, dire "Placer cette commande avec 10% de réduction" sans le domaine de la validation que le rabais doit aller de l'avant. Une chose qui a vraiment frappé la maison pour moi a été quelque chose que l'Udi lui-même dit à moi (vérifier les commentaires de la accepté de répondre).
Fondamentalement, je suis venu à réaliser qu'il n'y a aucune raison de ne pas faire confiance au client; le tout sur le côté lecture a été produite à partir du modèle de domaine, donc il n'y a pas de raison de ne pas accepter les commandes. Que ce soit dans le côté lecture qui dit que le client remplit les conditions requises pour la remise a été mis là par le domaine.
Si vous voulez adopter event sourcing & la cohérence des résultats, vous devez accepter que, parfois, il ne sera pas possible d'afficher des messages d'erreur instantanément après l'envoi d'une commande. Avec le nom d'utilisateur unique exemple les chances que cela se produise sont tellement mince (étant donné que vous vérifiez le côté lecture avant l'envoi de la commande) ne vaut pas s'inquiéter trop, mais une notification ultérieure devra être envoyé pour ce scénario, ou peut-être leur demander un nom d'utilisateur différent la prochaine fois qu'ils se connectent. La grande chose à propos de ces scénarios est qu'il pousse à réfléchir sur la valeur de l'entreprise & ce qui est vraiment important.
Mise à JOUR : Oct 2015
Voulais juste ajouter, que, dans la réalité, où les sites web sont concernés, ce qui indique que l'e-mail est déjà prise est fait à l'encontre des meilleures pratiques de sécurité. Au lieu de cela, l'enregistrement doit semblent avoir traversé avec succès informant l'utilisateur qu'une vérification email a été envoyé, mais dans le cas où le nom d'utilisateur existe, l'email doit en informer et les inciter à la connexion ou la réinitialisation de leur mot de passe. Bien que cela ne fonctionne que lors de l'utilisation d'adresses e-mail comme nom d'utilisateur, ce qui je pense est conseillé pour cette raison.
Il n'y a rien de mal avec la création de certains ont une lecture cohérente de modèles (par exemple, pas sur un réseau distribué) mis à jour dans le paiement de la commande.
Avoir lu les modèles éventuellement être cohérent sur un réseau distribué aide à soutenir la mise à l'échelle de la lecture modèle de lourds systèmes de lecture. Mais il n'y a rien à dire que vous ne pouvez pas avoir un domaine spécifique de lecture modèle des thats immédiatement compatibles.
Immédiatement la lecture cohérente modèle n'est jamais utilisé pour vérifier les données avant d'émettre une commande, vous ne devez jamais utiliser directement l'affichage de lire les données d'un utilisateur (c'est à dire à partir d'une requête web ou similaire). Utiliser finalement cohérente, évolutive lire les modèles de pour que.
Comme beaucoup d'autres lors de la mise en œuvre d'un événement de source en fonction du système que nous avons rencontré le problème de l'unicité.
Au début, j'étais un partisan de laisser le client d'accéder à la requête de côté avant l'envoi d'une commande pour savoir si un nom d'utilisateur est unique ou pas. Mais ensuite, j'ai commencé à voir que le fait d'avoir un back-end qui a zéro de validation sur l'unicité est une mauvaise idée. Pourquoi imposer quoi que ce soit quand il est possible de poster une commande qui permette de corrompre le système ? Un back-end doivent valider toutes les c'est l'entrée d'autre que vous êtes ouvert pour les données incohérentes.
Ce que nous avons fait a été de créer un
index
à l'invite de côté. Par exemple, dans le cas simple d'un nom d'utilisateur qui doit être unique, il suffit de créer un UserIndex avec un champ nom d'utilisateur. Maintenant la commande de côté de pouvez vérifier si un nom d'utilisateur est déjà dans le système ou non. Après la commande a été exécutée, il est plus sûr de stocker le nouveau nom d'utilisateur dans l'index.Quelque chose comme ça pourrait aussi travailler pour la Commande d'actualisation problème.
Les avantages sont que votre commande de back-end valide correctement tous les commentaires donc pas incohérent de données pourraient être stockées.
Un inconvénient pourrait être que vous avez besoin d'une requête supplémentaire pour chaque contrainte d'unicité et de vous faire respecter supplémentaire de complexité.
Je pense que pour de tels cas, on peut utiliser un mécanisme tel que "consultatif de verrouillage avec de l'expiration".
Exemple d'exécution:
Même vous pouvez utiliser une base de données sql; insérer le nom d'utilisateur en tant que clé primaire de certains de verrouillage de la table, et puis une tâche planifiée peut gérer les expirations.
Sur l'originalité, j'ai mis en place les éléments suivants:
Une première commande, comme "StartUserRegistration". UserAggregate serait créé, peu importe si l'utilisateur est unique ou pas, mais avec un statut de RegistrationRequested.
Sur "UserRegistrationStarted" un message asynchrone serait envoyé à un apatride, de service "UsernamesRegistry". serait quelque chose comme "RegisterName".
Service essaient de mettre à jour (pas de question, "dire de ne pas demander") le tableau qui comprendrait une contrainte unique.
En cas de succès, le service permettrait de répondre à un autre message (de manière asynchrone), avec une sorte d'autorisation "UsernameRegistration", en indiquant que le nom d'utilisateur a été correctement enregistré. Vous pouvez inclure quelques iddemande à suivre en cas de compétence concurrente (peu probable).
L'émetteur du message ci-dessus a maintenant une autorisation que le nom a été enregistré par lui-même alors maintenant, peut en toute sécurité marque de la UserRegistration ensemble comme un succès. Sinon, marquer comme rejetés.
Habillage:
Cette approche n'implique pas de requêtes.
D'enregistrement de l'utilisateur serait toujours créés avec aucune validation.
Processus de confirmation impliquerait deux messages asynchrones et un db de l'insertion. La table ne fait pas partie d'un modèle de lire, mais d'un service.
Enfin, une commande asynchrone pour confirmer que l'Utilisateur est valide.
À ce stade, un denormaliser pourrait réagir à une UserRegistrationConfirmed de l'événement et de créer un modèle de lire pour l'utilisateur.
Avez-vous songé à l'aide d'un "travail" cache comme une sorte de RSVP? C'est difficile à expliquer, car il travaille dans un peu d'un cycle, mais au fond, quand un nouveau nom d'utilisateur est "revendiquée" (qui est, la commande a été émise pour la créer), vous placez le nom d'utilisateur dans le cache avec une expiration courte (assez long pour le compte d'une autre demande de passer à travers la file d'attente et dénormalisée dans la lecture du modèle). Si c'est une instance de service, puis dans la mémoire serait probablement travailler, sinon centraliser avec Redis ou quelque chose.
Alors que le prochain utilisateur remplit le formulaire (en supposant qu'il est un front-end), vous asynchrone vérifier le modèle de lire pour connaître la disponibilité d'un nom d'utilisateur et d'alerter l'utilisateur si elle est déjà prise. Lorsque la commande est envoyée, vous vérifiez que le cache (pas le lire modèle) afin de valider la demande avant d'accepter la commande (avant de retourner 202); si le nom est dans le cache, ne pas accepter la commande, si ce n'est pas ajouter ensuite le cache; si l'ajout d'échec (double de la clé, parce que certains processus de vous battre pour cela), alors supposer le nom est pris, puis répondre au client de manière appropriée. Entre les deux choses, je ne pense pas qu'il va y avoir beaucoup de chances de collision.
Si il n'y a pas de front de la fin, alors vous pouvez sauter l'async rechercher ou, au moins, ont votre API fournir le point de terminaison pour le chercher. Vous ne devriez pas être en permettant au client de s'adresser directement au modèle de commande de toute façon, et en plaçant une API en face de vous permettrait d'avoir de l'API pour agir en tant que médiateur entre la commande et de lire des hôtes.
Il me semble que peut-être l'agrégat est mal ici.
En termes généraux, si vous avez besoin pour garantir que la valeur de Z appartenant à la Y est unique dans X, alors X d'usage sur l'ensemble. X, après tout, est l'endroit où l'invariant existe vraiment (un seul Z peut être dans X).
En d'autres termes, votre invariant est qu'un nom d'utilisateur ne peut apparaître qu'une fois dans le champ d'application de l'ensemble des utilisateurs de votre application (ou peut être un autre champ d'application, comme au sein d'une Organisation, etc.) Si vous avez un agrégat "ApplicationUsers" et d'envoyer le "RegisterUser de la commande", alors vous devriez être en mesure d'avoir ce dont vous avez besoin afin de s'assurer que la commande est valide avant de ranger le "UserRegistered" de l'événement. (Et, bien sûr, vous pouvez ensuite utiliser cet événement pour créer les projections dont vous avez besoin pour faire des choses telles que l'authentification de l'utilisateur sans avoir à charger l'ensemble de la "ApplicationUsers" agrégat.