You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Je vais décrire le fonctionnement de Rdd tel que je le vois dans l'idéal. Ensuite vous me direz ce que vous en pensez. Et une fois qu'on sera alignés, on verra en quoi il est différent de Rdd v3 et on saura alors quoi refactorer !
Architecture en couches
Rdd possède 4 couches qui ont chacune des responsabilités bien différentes :
Web, qui expose la logique Rdd via des API REST. Elle gère la transformation d'une requête entrante vers un appel de code à la couche Application, puis gère la sérialisation de la réponse en Json. C'est égalament dans cette couche qu'on gère l'enregistrement de l'injection, étant donné que c'est la couche qui fait tourner l'application.
Application, qui expose la logique Rdd sous forme de code C# mais en empruntant le style architectural REST. De ce point de vue, les appels HTTP des controllers web sont routés en 1 pour 1 vers des méthodes correspondantes des controllers applicatifs. Cette couche gère les échanges avec l'Infra, notamment la sauvegarde des données (base, disque). C'est également elle qui valide les données en provenance de la couche Web afin de s'assurer qu'elles sont viables, car le Domain ne fonctionne qu'avec des données valides.
Domain, qui expose des entités en C# à travers des collections. Chaque entité est pilotée par une collection qui fait office de factory en écriture, et contrôle les accès aux entités en lecture. On ne doit pas pouvoir récupérer ou écrire des entités autrement qu'en passant par sa collection. La collection expose des méthodes générique pour les appels provenant de Web, et des méthodes explicites pour les appels C#. Le Domain est agnostic de la couche de persistance (base, disque). Les entités sont des objets riches qui contiennent un état et un comportement. Ils sont notamment capable de se valider eux-mêmes.
Infra, qui regroupe toutes les implémentations des abstractions dont les 3 autres couches ont besoin. On pourrait d'ailleurs éclater cette couche en des projets spécialisés chacun dans leur techno, de sorte qu'un projet client ne soit pas contraint de dépendre de toutes les librairies tierces utilisées par Infra.
Réponses HTTP
Rdd supporte les 4 verbes HTTP, et fait même une distinction sur le GET entre un GET vers une collection ou vers une entité en particulier.
Les controllers web exposent donc 5 routes (Get, GetById, Put, Post, Delete). Ils n'ont pas vocation à en exposer plus. Si tel est le cas, cela signifie qu'il faut créer une sous entité pour matérialiser le process qui se dessine.
Get renverra une 404 uniquement si le type d'entité n'existe pas (route inexistante).
GetById renverra une 404 si l'id ne correspond à aucun entité existante. Si une entité à été supprimée, alors son URL devra renvoyer une 404. Si en revanche elle est inactive, càd qu'on peut encore y accéder en passant un paramètre spécifique à la collection, alors son URL devra renvoyer une 200.
L'utilisation de Put n'est pas conseillée. Put ne doit être utilisé qu'en cas de modification de l'état d'une entité n'entraînant pas un processus métier identifié. Dans le cas contraire, il faut alors créer une nouvelle entité qui représentera ce processus métier et faire un Post sur cette nouvelle entité.
Put prend en entrée des clés/valeurs correspondant aux propriétés de l'entité, et renvoie en sortie l'entité complète dans son nouvel état.
Post permet de créer une entité. Actuellement il impose d'envoyer des clés/valeurs correspondant aux propriétés de l'entité. On pourrait imaginer qu'il soit bindé aux constructeurs de l'entité, et donc qu'on doivent envoyer des clés/valeurs correspondant aux paramètres des différents contructeurs. Ainsi on pourrait à la fois ouvrir un peu les contraintes en acceptant des paramètres ne correspondant pas systématiquement à des propriétés de l'entité, mais également renforcer les contraintes en n'acceptant QUE des combinaisons de paramètres correspondant à des constructeurs explicites. Ceci garantirait une sécurité très forte par rapport à la situation actuelle.
Un Post renvoie la nouvelle entité dans son état final.
Delete permet de supprimer une entité. Qu'il s'agisse d'une suppression logique ou physique, le fait d'être passé par un Delete implique que l'entité n'est plus accessible par aucun moyen. Son URL directe (avec son Id) renvoie une 404. C'est pour cette raison qu'un Delete ne renvoie pas l'entité, car elle n'existe plus. Il renvoie un 200 OK vide.
Rdd supporte les requêtes multiples. Elles sont atomiques, càd que si une seule plante, aucune modification n'est enregistrée pour aucune requête. Voici les détails pour chaque verbe :
un Delete multiple prend en entrée la liste des Id des entités à supprimer
un Post multiple prend en entrée une liste d'objets contenant des clés/valeurs correspondant aux entités à créer
un Put multiple prend en entrée une liste d'objets contenant pour chacun l'id de l'entité à modifier ainsi que les clés/valeurs des propriétés à modifier
Pour faire des requêtes multiples non atomiques, càd indépendantes les unes des autres, il faut créer une route spécifique, par ex /batchs, qu'on utilise toujours en Post, en envoyant des objets décrivant les requêtes qu'on veut faire, par ex [ { "method": "Put", "href": "/api/users/123", "data": { "address": "newAddress", "phoneNumber": "01 02 03 04 05" } }, ... ]. Cette feature n'est actuellement pas implémentée par Rdd.
Si une route n'est pas décrite explicitement dans le controller web, elle devrait planter en 404.
Controller Applicatif
Les controllers applicatif sont les seuls points d'entrée dans une application Rdd. En général appelé depuis la couche Web, mais également depuis des tests. Ils peuvent également être appelé depuis un script ou une application console. Ils représentent la surface d'exposition publique de l'application.
Si un controller A veut appeler un controller B, alors la méthode exposée sur B devra être internal de sorte qu'elle ne soit pas exposée au monde extérieur.
Si un même repo GitHub comporte plusieurs applications, avec des projets mutualisés, il ne faut pas appeler un controller B d'une appli P2 depuis un controller A d'une appli P1 ! Si P1 et P2 sont 2 applis qui tournent séparément (ont chacune leur projet web), alors P1 ne peut appeler P2 qu'en faisant une requête HTTP, de sorte de ne pas faire tourner une partie de P2 dans P1 !
Un controller applicatif permet de séquencer un ou plusieurs appels vers des collections. Néanmoins, il est important de noter que si un concept émerge dans la couche Application sans exister dans le Domain, ce n'est pas normal. Il convient alors de créer une collection et une entité dédiée à ce concept et ainsi avoir un controller applicatif qui ne fait qu'un seul appel au lieu de 2 ou 3.
Le controller a également comme responsabilité de valider les données provenant de Web avant d'appeler Domain, de sorte que le Domain ne fonctionne que sur des données valides.
Enfin, il décide de persister les données, de sorte que le Domain n'ait jamais à s'en préoccuper.
Si des traitements sont à lancer après la persistance, notamment l'envoi des mails de notif ou encore l'émission d'un événement (EventBroker power !) alors ce sera également au controller applicatif de le gérer.
Collections du Domain
Les Collections du Domain représentent les points d'entrées vers le Domain. Elles permettent de centraliser l'accès aux entités, et également de leur injecter des services en dépendance.
Elles peuvent s'appuyer sur des helpers, notamment pour l'instanciation des entités et/ou leur patching leur d'un Put.
Si on applique la logique exposée plus haut à propos de l'instanciation des entités (via leur constructeur uniquement), et si ce sont les Controllers applicatifs qui lancent la validation des entités, alors la création se résume à vérifier les droits et ajouter l'entité au Repo.
Lors d'un Put, l'entité est remontée par le Repo, donc censée être valide, puis patchée dans la Collection, qui doit alors la valider. Ici aussi, il faut vérifier les droits, c'est fait lors de la remontée de l'entité.
En lecture, la Collection passe la Query (en provenance de la couche Web) au Repo qui va alors générer une requête SQL correspondante. Les droits sont également joués par le Repo (au plus proche des données).
Entités
Le concept d'entité arrive tout droit du DDD, et nous essayons de le respecter au maximum. Pour ce faire, une entité a forcément un Id, ce qui permet de l'identifier de façon unique. Etant donné qu'on est dans un paradigme REST, les entités ont également un nom et une URL en sortie de la couche Web.
Une Entité doit posséder tout ce qui permet son bon fonctionnement, et n'est pilotée que par sa collection. L'entité doit être capable de gérer son état, ainsi que ses modifications (via des méthodes appropriée). Elle doit aussi gérer sa validation et son instanciation.
Elle peut s'appuyer sur des ValueObjects pour mieux organiser son état.
Elle peut également gérer des sous entités notamment s'il s'agit de commandes.
Repo de l'Infra
Les Repos, à l'instar du concept éponyme dans le DDD, sont là pour reconstruire les entités du Domain lors d'un Get, et pour assurer leur persistance en écriture.
En lecture, ils interprètent la Query qui vient du Web pour la traduire en requête SQL. On peut décomposer le Get en étapes :
le Set() permet à une Repo d'aller chercher les entités, indépendamment de la Query ou du user qui fait la requête. C'est l'équivalent d'un SELECT * FROM T en SQL.
ApplyRights() permet de jouer les droits sur les entités par rapport au user connecté
ApplyFilters() va parser la Query et transformer les filtres web en filtres SQL sur les entités
ApplyOrderBys() et ApplyPage() gèrent le tri et le paging
ApplyIncludes() permet d'inclure des objets présents en base dans des autres tables que la table principale hébergeant les entités. Attention à ne pas ramener d'autres entités qui seraient alors dénuées de toute gestion des droits !
Un Repo s'appuie sur un IStorageService pour savoir où sont les données et permettre d'en ajouter. Il n'est plus responsable de persister les données. Cette responsabilité a été transférée à la couche Application, qui utilise un IUnitOfWork, en général branché sur le même DbContext que les Repos, pour persister effectivement les données.
Query
La Query est un objet central dans Rdd, car il matérialise sous forme d'objet la requête Web. Il est interprété par les Repos mais passe à travers le controller applicatif et la collection. C'est notamment pour cela qu'il est dans le Domain.
Néanmoins, quand une requête à une collection est faite depuis une autre collection ou un controller applicatif, on sait exactement dans quel cadre on se situe, et donc on n'a pas besoin d'un objet Query :
pas besoin de Fields puisqu'on récupère un objet C# complet
pas besoin de filtres puisqu'on peut créer une méthode explicite qui prend des filtres explicites en paramètre
pas besoin de orderby ni de paging puisqu'on est en C#, on peut faire ça dans une méthode explicite du Repo
pas besoin des options, notamment CheckRights puisqu'encore une fois, étant dans des méthodes explicites, on peut préciser ça dans le Repo explicitement
Au final, on pourrait interdire l'usage de Query depuis le Domain, et avoir 2 façons d'interagir avec des entités :
chemin générique, matérialisé par Query, et interprété par le Repo
chemin explicite, matérialisé par des méthodes explicites à tous les niveaux (controller, collection, repo)
The text was updated successfully, but these errors were encountered:
Avant tout, je rajouterai que RDD est un framework destiné à simplifier la création d'api d'une application
Couche Web:
gère l'authentication et le routing
Réponses HTTP:
Je préciserai que RDD supporte le post, le put et le delete en masse, et que, concernant le post et le put, c'est la version en masse qui doit être prioritisée, car c'est celle ou RDD à le plus de valeur à ajouter, notamment en garantissant que les optimisations de perf sur le sujet sont garanties.
Query:
je dirais plutot que Query est censé correspondre au QuerySpecification pattern, au sens ou rien de web n'est censé transpirer de cet objet.
Je suis pour rendre impossible la construction de Query depuis les couches domain.
Je vais décrire le fonctionnement de Rdd tel que je le vois dans l'idéal. Ensuite vous me direz ce que vous en pensez. Et une fois qu'on sera alignés, on verra en quoi il est différent de Rdd v3 et on saura alors quoi refactorer !
Architecture en couches
Rdd possède 4 couches qui ont chacune des responsabilités bien différentes :
Web, qui expose la logique Rdd via des API REST. Elle gère la transformation d'une requête entrante vers un appel de code à la couche Application, puis gère la sérialisation de la réponse en Json. C'est égalament dans cette couche qu'on gère l'enregistrement de l'injection, étant donné que c'est la couche qui fait tourner l'application.
Application, qui expose la logique Rdd sous forme de code C# mais en empruntant le style architectural REST. De ce point de vue, les appels HTTP des controllers web sont routés en 1 pour 1 vers des méthodes correspondantes des controllers applicatifs. Cette couche gère les échanges avec l'Infra, notamment la sauvegarde des données (base, disque). C'est également elle qui valide les données en provenance de la couche Web afin de s'assurer qu'elles sont viables, car le Domain ne fonctionne qu'avec des données valides.
Domain, qui expose des entités en C# à travers des collections. Chaque entité est pilotée par une collection qui fait office de factory en écriture, et contrôle les accès aux entités en lecture. On ne doit pas pouvoir récupérer ou écrire des entités autrement qu'en passant par sa collection. La collection expose des méthodes générique pour les appels provenant de Web, et des méthodes explicites pour les appels C#. Le Domain est agnostic de la couche de persistance (base, disque). Les entités sont des objets riches qui contiennent un état et un comportement. Ils sont notamment capable de se valider eux-mêmes.
Infra, qui regroupe toutes les implémentations des abstractions dont les 3 autres couches ont besoin. On pourrait d'ailleurs éclater cette couche en des projets spécialisés chacun dans leur techno, de sorte qu'un projet client ne soit pas contraint de dépendre de toutes les librairies tierces utilisées par Infra.
Réponses HTTP
Rdd supporte les 4 verbes HTTP, et fait même une distinction sur le GET entre un GET vers une collection ou vers une entité en particulier.
Les controllers web exposent donc 5 routes (Get, GetById, Put, Post, Delete). Ils n'ont pas vocation à en exposer plus. Si tel est le cas, cela signifie qu'il faut créer une sous entité pour matérialiser le process qui se dessine.
Get renverra une 404 uniquement si le type d'entité n'existe pas (route inexistante).
GetById renverra une 404 si l'id ne correspond à aucun entité existante. Si une entité à été supprimée, alors son URL devra renvoyer une 404. Si en revanche elle est inactive, càd qu'on peut encore y accéder en passant un paramètre spécifique à la collection, alors son URL devra renvoyer une 200.
L'utilisation de Put n'est pas conseillée. Put ne doit être utilisé qu'en cas de modification de l'état d'une entité n'entraînant pas un processus métier identifié. Dans le cas contraire, il faut alors créer une nouvelle entité qui représentera ce processus métier et faire un Post sur cette nouvelle entité.
Put prend en entrée des clés/valeurs correspondant aux propriétés de l'entité, et renvoie en sortie l'entité complète dans son nouvel état.
Post permet de créer une entité. Actuellement il impose d'envoyer des clés/valeurs correspondant aux propriétés de l'entité. On pourrait imaginer qu'il soit bindé aux constructeurs de l'entité, et donc qu'on doivent envoyer des clés/valeurs correspondant aux paramètres des différents contructeurs. Ainsi on pourrait à la fois ouvrir un peu les contraintes en acceptant des paramètres ne correspondant pas systématiquement à des propriétés de l'entité, mais également renforcer les contraintes en n'acceptant QUE des combinaisons de paramètres correspondant à des constructeurs explicites. Ceci garantirait une sécurité très forte par rapport à la situation actuelle.
Un Post renvoie la nouvelle entité dans son état final.
Delete permet de supprimer une entité. Qu'il s'agisse d'une suppression logique ou physique, le fait d'être passé par un Delete implique que l'entité n'est plus accessible par aucun moyen. Son URL directe (avec son Id) renvoie une 404. C'est pour cette raison qu'un Delete ne renvoie pas l'entité, car elle n'existe plus. Il renvoie un 200 OK vide.
Rdd supporte les requêtes multiples. Elles sont atomiques, càd que si une seule plante, aucune modification n'est enregistrée pour aucune requête. Voici les détails pour chaque verbe :
Pour faire des requêtes multiples non atomiques, càd indépendantes les unes des autres, il faut créer une route spécifique, par ex /batchs, qu'on utilise toujours en Post, en envoyant des objets décrivant les requêtes qu'on veut faire, par ex
[ { "method": "Put", "href": "/api/users/123", "data": { "address": "newAddress", "phoneNumber": "01 02 03 04 05" } }, ... ]
. Cette feature n'est actuellement pas implémentée par Rdd.Si une route n'est pas décrite explicitement dans le controller web, elle devrait planter en 404.
Controller Applicatif
Les controllers applicatif sont les seuls points d'entrée dans une application Rdd. En général appelé depuis la couche Web, mais également depuis des tests. Ils peuvent également être appelé depuis un script ou une application console. Ils représentent la surface d'exposition publique de l'application.
Si un controller A veut appeler un controller B, alors la méthode exposée sur B devra être
internal
de sorte qu'elle ne soit pas exposée au monde extérieur.Si un même repo GitHub comporte plusieurs applications, avec des projets mutualisés, il ne faut pas appeler un controller B d'une appli P2 depuis un controller A d'une appli P1 ! Si P1 et P2 sont 2 applis qui tournent séparément (ont chacune leur projet web), alors P1 ne peut appeler P2 qu'en faisant une requête HTTP, de sorte de ne pas faire tourner une partie de P2 dans P1 !
Un controller applicatif permet de séquencer un ou plusieurs appels vers des collections. Néanmoins, il est important de noter que si un concept émerge dans la couche Application sans exister dans le Domain, ce n'est pas normal. Il convient alors de créer une collection et une entité dédiée à ce concept et ainsi avoir un controller applicatif qui ne fait qu'un seul appel au lieu de 2 ou 3.
Le controller a également comme responsabilité de valider les données provenant de Web avant d'appeler Domain, de sorte que le Domain ne fonctionne que sur des données valides.
Enfin, il décide de persister les données, de sorte que le Domain n'ait jamais à s'en préoccuper.
Si des traitements sont à lancer après la persistance, notamment l'envoi des mails de notif ou encore l'émission d'un événement (EventBroker power !) alors ce sera également au controller applicatif de le gérer.
Collections du Domain
Les Collections du Domain représentent les points d'entrées vers le Domain. Elles permettent de centraliser l'accès aux entités, et également de leur injecter des services en dépendance.
Elles peuvent s'appuyer sur des helpers, notamment pour l'instanciation des entités et/ou leur patching leur d'un Put.
Si on applique la logique exposée plus haut à propos de l'instanciation des entités (via leur constructeur uniquement), et si ce sont les Controllers applicatifs qui lancent la validation des entités, alors la création se résume à vérifier les droits et ajouter l'entité au Repo.
Lors d'un Put, l'entité est remontée par le Repo, donc censée être valide, puis patchée dans la Collection, qui doit alors la valider. Ici aussi, il faut vérifier les droits, c'est fait lors de la remontée de l'entité.
En lecture, la Collection passe la Query (en provenance de la couche Web) au Repo qui va alors générer une requête SQL correspondante. Les droits sont également joués par le Repo (au plus proche des données).
Entités
Le concept d'entité arrive tout droit du DDD, et nous essayons de le respecter au maximum. Pour ce faire, une entité a forcément un Id, ce qui permet de l'identifier de façon unique. Etant donné qu'on est dans un paradigme REST, les entités ont également un nom et une URL en sortie de la couche Web.
Une Entité doit posséder tout ce qui permet son bon fonctionnement, et n'est pilotée que par sa collection. L'entité doit être capable de gérer son état, ainsi que ses modifications (via des méthodes appropriée). Elle doit aussi gérer sa validation et son instanciation.
Elle peut s'appuyer sur des ValueObjects pour mieux organiser son état.
Elle peut également gérer des sous entités notamment s'il s'agit de commandes.
Repo de l'Infra
Les Repos, à l'instar du concept éponyme dans le DDD, sont là pour reconstruire les entités du Domain lors d'un Get, et pour assurer leur persistance en écriture.
En lecture, ils interprètent la Query qui vient du Web pour la traduire en requête SQL. On peut décomposer le Get en étapes :
Set()
permet à une Repo d'aller chercher les entités, indépendamment de la Query ou du user qui fait la requête. C'est l'équivalent d'unSELECT * FROM T
en SQL.ApplyRights()
permet de jouer les droits sur les entités par rapport au user connectéApplyFilters()
va parser la Query et transformer les filtres web en filtres SQL sur les entitésApplyOrderBys()
etApplyPage()
gèrent le tri et le pagingApplyIncludes()
permet d'inclure des objets présents en base dans des autres tables que la table principale hébergeant les entités. Attention à ne pas ramener d'autres entités qui seraient alors dénuées de toute gestion des droits !Un Repo s'appuie sur un
IStorageService
pour savoir où sont les données et permettre d'en ajouter. Il n'est plus responsable de persister les données. Cette responsabilité a été transférée à la couche Application, qui utilise unIUnitOfWork
, en général branché sur le mêmeDbContext
que les Repos, pour persister effectivement les données.Query
La Query est un objet central dans Rdd, car il matérialise sous forme d'objet la requête Web. Il est interprété par les Repos mais passe à travers le controller applicatif et la collection. C'est notamment pour cela qu'il est dans le Domain.
Néanmoins, quand une requête à une collection est faite depuis une autre collection ou un controller applicatif, on sait exactement dans quel cadre on se situe, et donc on n'a pas besoin d'un objet Query :
CheckRights
puisqu'encore une fois, étant dans des méthodes explicites, on peut préciser ça dans le Repo explicitementAu final, on pourrait interdire l'usage de Query depuis le Domain, et avoir 2 façons d'interagir avec des entités :
The text was updated successfully, but these errors were encountered: