| Le PetShopDNG 2.0, l'architecture multi-tiers en action par Thomas GIL (thomas.gil@valtech.fr) | ||

En début d'année 2003, le PetShopDNG 1.0 a posé les bases d'un modèle d'application à plusieurs couches logiques, déployé sur 3 couches physiques (navigateur Web, serveur Web, base de données).
A sa sortie, nombreux sont ceux parmi vous qui ont regretté que cette application "de référence" repose partiellement sur un produit commercial, Evaluant DTM (même si ce produit est récemment devenu semi-libre, comme vous avez pu le lire sur DotNetGuru).
D'autre part, nous étions nous-mêmes restés un peu sur notre faim puisque le PetShop 1.0 ne tirait pas parti de .NET Remoting, empêchant par là même tout déploiement en 4 couches physiques.
Cet article accompagne la sortie de la nouvelle mouture de notre petite application : le PetShopDNG 2.0. Il a donc les objectifs suivants :
bien entendu décrire les nouveautés que vous découvrirez dans le code du PetShopDNG 2.0
montrer en quoi l'approche par interfaces et Abstract Factory que nous avions suivie dans le PetShopDNG 1.0 a été gagnante
expliquer les choix concernant l'implémentation de la couche d'accès aux données "faite maison" du nouveau PetShopDNG
expliquer également la conception de la couche de services distribués via .NET Remoting
enfin, mettre le doigt sur certaines rigidités des outils de mapping Objet/Relationnel qui nous ont contraint à un compromis difficilement acceptable dans une application non-académique
Les amateurs d'ASP.NET, quant à eux, seront probablement déçus : la couche de présentation du PetShopDNG n'a pas évolué depuis la version 1.0. La raison est simple : les choix que nous avions faits à l'époque (janvier 2003) n'ont pas de raison d'être remis en cause pour le moment (même si certains experts ont pu nous faire part de leur opinion concernant le chargement dynamique de contrôles utilisateurs). L'événement qui nous fera revenir sur cette position sera probablement la sortie du framework ASP.NET 2.0.
[Les lecteurs qui ont encore bien en tête cette architecture sont évidemment invités à reprendre leur lecture à la section suivante]
Le PetShop 1.0 était une petite application dans laquelle nous avons tenté de montrer comment bâtir une architecture multi-couches souple et évolutive. En partant du poste utilisateur, nous pouvons résumer les responsabilités de la manière suivante :
une page ASPX de "syndication de contenu" agrège un ensemble de contrôles ASCX, qui deviennent donc des portlets (chacun constitue en effet un élément du portail). Toutes les requêtes des clients ciblent cette page, qui a donc la responsabilité de gérer le contexte conversationnel entre le serveur et les clients.
chaque contrôle peut offrir des services (liste des catégories d'animaux à vendre, moteur de recherche) et donc déclencher des événements utilisateur.
les événements sont bien entendu traités dans les classes C# "CodeBehind" des contrôles, dans lesquelles on déclenche une activité, à l'issue de laquelle le système change d'état. Dès lors, la page de "syndication de contenu" fait en sorte d'afficher les portlets requises pour la présentation de ce nouvel état.
étant donné que la même action est susceptible d'être déclenchée de diverses manières à travers notre portail, les CodeBehind s'appuient en réalité sur une couche d'abstraction supplémentaire, les "WebCommands", qui sont de simples classes C# chargées de solliciter les contrôleurs de cas d'utilisation, et de gérer la navigation à travers notre portail.
les contrôleurs de cas d'utilisation sont des classes C# façades, responsables de l'interaction fine avec le modèle des objets métier. En pratique, les WebCommands ne manipulent que les interfaces des contrôleurs de cas d'utilisation : couplées à une AbstractFactory, ces interfaces permettent de limiter énormément le couplage entre les WebCommands et l'implémentation des contrôleurs. Nous reviendrons sur cet aspect dans la section suivante.
les objets métier, eux aussi, se cachent derrière leurs interfaces. Leur code est trivial : notre site est pauvre en logique, le plus complexe revenant certainement à gérer le caddie virtuel de l'utilisateur !
et enfin, la persistance des objets métier est réalisée par l'outil de mapping Objet/Relationnel DTM, de la société Evaluant. Malheureusement, comme DTM ne permet pas de générer des classes qui implémentent nos interfaces, nous avons été obligés de recourir à une technique d'adaptation, le décorateur, qui fait le lien entre nos interfaces métier et les classes générées par l'outil.
Le diagramme suivant reprend ces éléments de manière un peu plus synthétique :
|
|
Toute l'implémentation du PetShopDNG 2.0 repose sur le Design Pattern Abstract Factory, qui constitue une véritable charnière. En effet, cette nouvelle version propose 4 implémentations différentes des couches service, métier et accès aux données, que nous reprendrons sous les acronymes BLL (Business Logic Layer) et DAL (Data Access Layer) :
En mode "3 couches physiques utilisant un outil de mapping Objet / Relationnel", nous avons complété l'implémentation basée sur DTM par une nouvelle version basée sur Norpheme (cette implémentation a été réalisée par Matthieu Guyonnet-Duluc, qui connaît le PetShopDNG encore mieux que son auteur ;-) ...)
Afin de satisfaire les personnes qui souhaitaient une implémentation "cousue main" de la couche de persistance, nous avons implémenté notre propre couche d'accès aux données (sur laquelle nous allons revenir plus en détail dans les sections suivantes)
Et enfin, grâce à .NET Remoting, il est dorénavant possible de déployer le PetShopDNG sur 4 couches physiques : la couche de services et d'accès aux données sont effectivement distribuables sur une autre machine que celle qui héberge la couche de présentation ASP.NET.
Là où la beauté du Pattern Abstract Factory est resplendissante, c'est que pour passer d'une implémentation à l'autre, il suffit de modifier le fichier de configuration Web.config ! En effet, dès la modification des informations de configuration, le PetShopDNG détecte quelle nouvelle implémentation il doit offrir derrière les interfaces des couches BLL et DAL; la couche de présentation ASP.NET, elle, n'y voit que du feu puisque sa vision se limite à celle des fabriques abstraites et des interfaces des couches de service et d'objets du domaine.
Pour vous faire une idée des informations que requiert le mécanisme de configuration dynamique (toujours basé sur la Reflexion .NET), voici le contenu du fichier Web.config :
|
<?xml version="1.0"
encoding="utf-8" ?> |
| Web.config |
Résultat des courses, il faut se forcer à ne manipuler que des interfaces depuis la couche de présentation, passer systématiquement par l'intermédiaire de fabriques abstraites et implémenter les interfaces dans les couches d'implémentation BLL et DAL :
constitue une charge de travail supplémentaire, qui peut paraître lourde
mais cette discipline est structurante, elle force à limiter le couplage entre deux couches successives (entre la couche de présentation ASP.NET d'un côté, et les couches BLL et DAL dans notre cas)
et nous voyons que la promesse de pouvoir changer d'implémentation sans aucune répercussion sur la couche utilisatrice (présentation ici) est tenue. Quelle souplesse ! Quel pattern élégant !
Comment implémenter une couche d'accès aux données à la fois simple, souple et efficace ? Voici les choix de conception que nous avons faits dans le PetShopDNG 2.0 :
comme pour les autres implémentations (reposant respectivement sur Evaluant DTM et Norpheme), il existe une classe implémentant chaque interface métier du namespace PetShopDNG.DAL.
ces classes implémentent également deux interfaces techniques, IPersistent et IDistributable, qui les obligent à redéfinir certaines propritétés (Id, IsTotallyLoaded et IsDistributed).
il existe une petite dépendance entre ces classes de la DAL et leurs Factories (sur lesquelles nous allons revenir dans quelques lignes). En effet, nos objets peuvent n'être que partiellement chargés (ils ne connaissent alors que leur Id), auquel cas dès qu'une autre propriété que l'identifiant est lue ou modifiée, il faut terminer le chargement en mémoire de l'état de l'objet; cela se fait en rappelant la méthode LoadObject(..) de la fabrique adéquate.
[Remarque : le code des objets métier est un peu alourdi par le fait qu'il faille invoquer LoadObject(..) dans tous les accesseurs de propriétés. Dans un monde idéal, c'est-à-dire si le projet AspectDNG était arrivé à maturité assez tôt, nous aurions pu simplifier ce code en greffant l'invocation de LoadObject(..) à chaque début de corps d'accesseur, autre que ceux de la propriété Id. Ce n'est que partie remise.]
Le diagramme de classes correspondant est très simple :
|
Figure B : Classes correspondant à l'implémentation "cousue main" de la couche d'accès aux données |
Le code des objets métier est très simple. Mais il n'en va pas de même pour leur Factories. Elles ont en effet la lourde tâche de réaliser le mapping entre la représentation Objet en mémoire des informations et la représentation relationnelle en base de données.
La gestion des aspects techniques nécessite une attention particulière. Nous nous sommes rapidement rendu compte qu'il était dangereux de dupliquer du code d'ouverture / fermeture des connexions à la base de données, de même qu'il était subtil de propager systématiquement les transactions à travers toutes les méthodes de nos fabriques. Le risque, en effet, était de ne pas gérer ces ressources de manière uniforme à travers le projet. Il a donc fallu factoriser (sans mauvais jeu de mot) ce code technique.
Mais ce n'est pas tout : il est évident que les différentes fabriques (de Catégories, de Produits, d'Items...) allaient se ressembler énormément. Chacune devrait être capable d'exécuter des requêtes SQL d'insertion, modification, suppression dans la base de données, une requête de recherche par clé primaire, et éventuellement quelques recherches sur d'autres critères. Si l'on y réfléchit bien, les besoins sont assez simples pour une factory :
dans l'étape de lecture de la base de données (recherche par clé ou par critère), il s'agit de lancer une requête, d'instancier un objet métier par ligne renvoyée par la base de données, et d'affecter à chaque propriété de l'objet la valeur du champ correspondant en base
dans l'étape d'insertion, c'est l'inverse : il faut être capable de déclencher une commande (et donc une requête SQL) en ajoutant des paramètres correspondant aux propriétés de l'objet à stocker ou à modifier.
Afin de simplifier (énormément) le code de chaque fabrique, nous avons donc décidé de développer une fabrique générique, qui ouvrirait les connexions, débuterait les transactions, etc... et délèguerait aux fabriques concrètes les responsabilités suivantes :
préciser le code des requêtes SQL à exécuter sur la base
instancier un objet du bon type, et le charger avec les champs contenus dans une ligne d'un DataReader (bref, faire l'adaptation Relationnel -> Objet)
ajouter les paramètres nécessaires à une Command pour insérer / mettre à jour un objet en base (adaptation Objet -> Relationnel)
L'organisation des classes Factories et utilitaires (commande et transaction génériques) est donc la suivante. Pour l'instant, ne faites pas attention à la partie distribuée : nous y reviendrons dans une prochaine section.
|
|
Quelques précisions s'imposent. Tout d'abord, la GenericCommand : le principal intérêt de cette classe est de simplifier l'ajout de paramètres à une IdbCommand. Au lieu d'avoir à instancier un paramètre supplémentaire, à lui préciser son nom, son type et sa valeur, et à l'ajouter à la liste des paramètres de la commande, il suffit avec la GenericCommand d'utiliser la notation suivante :
maCommandeGenerique["nomParam"] = valeurParam;
Et d'autre part, il faudrait détailler le comportement de la GenericFactory pour bien se rendre compte de ce qui a été mis en facteur. Pour cela, rien ne vaut la lecture de son code source : le voici.
| namespace PetShopDNG.DAL.DngImpl.Factories {using System.Collections; using System; using System.Data; using PetShopDNG.DAL; public abstract class GenericFactory : MarshalByRefObject { // SQL queries will be set in concrete factories constructors protected string sqlStore; protected string sqlUpdate; protected string sqlDelete; protected string sqlSelectById; protected string sqlCount; // O/R mapping methods will be overridden in concrete factories protected abstract IPersistent CreateBlankObject(); protected abstract void ObjectToCommandParameters(object obj, GenericCommand cmd); protected abstract void DataReaderRowToObject(IDataReader reader, object obj, GenericTransaction tx); // Technical internal services private IPersistent DataReaderRowToObject(IDataReader reader, GenericTransaction tx){ IPersistent obj = CreateBlankObject(); DataReaderRowToObject(reader, obj, tx); return obj; } private Random rndGen = new Random(); private string GenerateId(){ return Guid.NewGuid().ToString(); } // Public methods public static GenericTransaction BeginTransaction(){ return new GenericTransaction(); } // Query methods public virtual void Store(object obj, GenericTransaction tx){ if (obj != null){ bool txWasNull = (tx == null); if (txWasNull) tx = BeginTransaction(); GenericCommand cmd = new GenericCommand(tx); cmd.CommandText = sqlStore; IPersistent pobj = (IPersistent) obj; pobj.Id = GenerateId(); ObjectToCommandParameters(pobj, cmd); int nbModified = cmd.ExecuteNonQuery(); if (txWasNull) tx.Commit(); } } public virtual void Update(object obj, GenericTransaction tx){ if (obj != null){ bool txWasNull = (tx == null || tx.IsEnded); if (txWasNull) tx = BeginTransaction(); GenericCommand cmd = new GenericCommand(tx); cmd.CommandText = sqlUpdate; ObjectToCommandParameters(obj, cmd); int nbModified = cmd.ExecuteNonQuery(); if (txWasNull) tx.Commit(); } } public virtual void Delete(object obj, GenericTransaction tx){ if (obj != null){ bool txWasNull = (tx == null || tx.IsEnded); if (txWasNull) tx = BeginTransaction(); IPersistent pobj = (IPersistent) obj; GenericCommand cmd = new GenericCommand(tx); cmd.CommandText = sqlDelete; cmd["@id"] = pobj.Id; int nbModified = cmd.ExecuteNonQuery(); if (txWasNull) tx.Commit(); } } public int Count(GenericTransaction tx){ //... } public virtual void LoadObject(object obj, GenericTransaction tx){ bool txWasNull = (tx == null || tx.IsEnded); if (txWasNull) tx = BeginTransaction(); IPersistent pobj = (IPersistent) obj; GenericCommand cmd = new GenericCommand(tx); cmd["@id"] = pobj.Id; cmd.CommandText = sqlSelectById; using(IDataReader reader = cmd.ExecuteReader()){ if (reader.Read()){ DataReaderRowToObject(reader, obj, tx); tx[pobj.Id] = pobj; } reader.Close(); } if (txWasNull) tx.Commit(); if (pobj != null){ pobj.IsTotallyLoaded = true; } } protected object FindUnique(string sqlString, GenericTransaction tx){ bool txWasNull = (tx == null || tx.IsEnded); if (txWasNull) tx = BeginTransaction(); IPersistent result = null; GenericCommand cmd = new GenericCommand(tx); cmd.CommandText = sqlString; using(IDataReader reader = cmd.ExecuteReader()){ if (reader.Read()){ result = DataReaderRowToObject(reader, tx); tx[result.Id] = result; } } if (txWasNull) tx.Commit(); if (result != null){ result.IsTotallyLoaded = true; } return result; } } } |
| GenericFactory.cs |
Grâce à la mise en facteur de ces aspects techniques dans la GenericFactory, le code des fabriques concrètes est assez simple, jugez plutôt :
| namespace PetShopDNG.DAL.DngImpl.Factories {using System.Data; using PetShopDNG.DAL; public class AccountFactory : GenericFactory { private static AccountFactory instance = new AccountFactory(); public static AccountFactory Instance { get{return instance;} } private AccountFactory(){ sqlStore = @"INSERT INTO Account (id, login, password, firstname, lastname, streetaddress, postalcode, city, telephonenumber, email, iwantpettips, iwantmylist, favoritelanguage, fk_creditcard, fk_favoritecategory) VALUES (@id, @login, @password, @firstname, @lastname, @streetaddress, @postalcode, @city, @telephonenumber, @email, @iwantpettips, @iwantmylist, @favoritelanguage, @fk_creditcard, @fk_favoritecategory)"; sqlUpdate = @"UPDATE Account SET login = @login, password = @password, firstname = @firstname, lastname = @lastname, streetaddress = @streetaddress, postalcode = @postalcode, city = @city, telephonenumber = @telephonenumber, email = @email, iwantpettips = @iwantpettips, iwantmylist = @iwantmylist, favoritelanguage = @favoritelanguage, fk_creditcard = @fk_creditcard, fk_favoritecategory = @fk_favoritecategory WHERE id = @id"; sqlDelete = "DELETE FROM Account WHERE id = @id"; sqlSelectById = "SELECT * FROM Account WHERE id = @id"; sqlCount = "SELECT COUNT(*) FROM Account"; } // Redefine O/R mapping methods protected override IPersistent CreateBlankObject(){ return new Account(); } protected override void ObjectToCommandParameters(object obj, GenericCommand cmd){ Account account = (Account) obj; cmd["@id"] = account.Id; cmd["@login"] = account.Login; cmd["@password"] = account.Password; cmd["@firstname"] = account.FirstName; cmd["@lastname"] = account.LastName; cmd["@streetaddress"] = account.StreetAddress; cmd["@postalcode"] = account.PostalCode; cmd["@city"] = account.City; cmd["@telephonenumber"] = account.TelephoneNumber; cmd["@email"] = account.EMail; cmd["@iwantpettips"] = account.IWantPetTips; cmd["@iwantmylist"] = account.IWantMyList; cmd["@favoritelanguage"] = account.FavoriteLanguage; cmd["@fk_creditcard"] = (account.CreditCard != null) ? account.CreditCard.Id : null; cmd["@fk_favoritecategory"] = (account.FavoriteCategory != null) ? account.FavoriteCategory.Id : null; } protected override void DataReaderRowToObject(IDataReader reader, object obj, GenericTransaction tx){ Account account = (Account) obj; account.Id = reader["id"] as string; account.Login = reader["login"] as string; account.Password = reader["password"] as string; account.FirstName = reader["firstname"] as string; account.LastName = reader["lastname"] as string; account.StreetAddress = reader["streetaddress"] as string; account.PostalCode = reader["postalcode"] as string; account.City = reader["city"] as string; account.TelephoneNumber = reader["telephonenumber"] as string; account.EMail = reader["email"] as string; account.IWantPetTips = (bool) reader["iwantpettips"]; account.IWantMyList = (bool) reader["iwantmylist"]; account.FavoriteLanguage = reader["favoritelanguage"] as string; string creditCardId = reader["fk_creditcard"] as string; if (creditCardId != null) account.CreditCard = CreditCardFactory.Instance.FindById(creditCardId, tx); string favoriteCategoryId = reader["fk_favoritecategory"] as string; if (favoriteCategoryId != null) account.FavoriteCategory = CategoryFactory.Instance.FindById(favoriteCategoryId, tx); } // Override methods that modify database state to handle dependant objects public override void Store(object obj, GenericTransaction tx){ CreditCardFactory.Instance.Store(((Account) obj).CreditCard, tx);
base.Store(obj,
tx); public
override
void Delete(object
obj,
GenericTransaction tx){ public
override
void Update(object
obj,
GenericTransaction tx){ // Find methods
public Account
FindById(string
id,
GenericTransaction tx){ public
Account FindByLogin(string
login,
GenericTransaction tx){ |
| AccountFactory.cs |
Bien sûr, il est possible de rendre notre couche d'accès aux données encore plus générique, soit en utilisant la réflexion (comme Norpheme), soit en procédant par génération de code (comme DTM). Mais dans ce cas, la couche d'accès aux données devient un développement de framework, ce que nous nous sommes interdits dans le cadre de cette implémentation manuelle, ou une simple utilisation d'un framework de mapping Objet/Relationnel, ce qui est l'objectif des implémentations sur Norpheme et DTM.
Nous avons mentionné dans les sections précédentes que le pattern Abstract Factory masquait complètement l'implémentation des couches DAL et BLL. C'est exact, mais nous avons tout de même rencontré un problème de taille lors du passage d'une implémentation à l'autre de ces couches.
En effet, les outils de mapping Objet/Relationnel que nous avons utilisés pour réaliser les implémentations de la DAL (DTM et Norpheme) supposent tous deux que nos objets C# disposent d'un identifiant unique, accessible via une propriété, et qui correspond à une clé primaire dans la base de données. Jusque là, aucun problème. Mais ce qui est bien plus gênant, c'est que :
DTM suppose qu'une clé primaire est de type VARCHAR en base de données (et la gestion des clés uniques par défaut passe par l'utilisation de GUID)
Norpheme, lui, ne gère tout simplement pas la génération de clés uniques : il se repose sur la base, et requiert donc un champ entier, plus précisément un ID IDENTITY, incrémenté par la base elle-même.
Dès lors, les choix étaient assez limités :
soit nous acceptions de créer deux bases de données différentes, dotées d'un schéma adapté aux contraintes des outils de mapping O/R
soit il fallait ajouter aux tables existantes des colonnes symétriques de gestion des clés primaires et étrangères : à la fois sous forme d'entiers et de chaînes.
Afin de pouvoir changer d'implémentation de manière très souple au niveau de notre Abstract Factory, et de limiter l'effort d'administration de la base, nous avons choisi la deuxième option. Chaque table dispose d'une clé entière, et d'une clé textuelle. Mais bien entendu, ces clés n'ont aucune corrélation, et ne sont donc pas synchronisées.
Pour résoudre ce problème, nous avons implémenté un nouveau cas d'utilisation : la "migration des données", qui uniformise les clés et les relations à travers nos tables. Ce choix nécessaire ne nous satisfait toutefois pas complètement, car cela signifie qu'à un instant donné, deux serveurs Web différents ne peuvent pas choisir d'utiliser l'un Norpheme et l'autre DTM simultanément ! Il existe donc bel et bien un couplage entre notre application et la couche d'accès aux données.
Bref, nous restons sur notre faim, et avons été assez déçus de cette contrainte imposée par les outils de mapping O/R : on serait en droit d'attendre de tels outils qu'ils proposent de gérer des clés de tous types, entiers, chaînes, clés agrégées correspondant à plusieurs colonnes d'une table... Et que tout cela soit configurable à souhait (typiquement, que le choix ne soit pas global à un projet, mais qu'il puisse être revu au cas par cas, objet par objet). Gageons que DTM, Evaluant et leurs concurrents intègrent ce besoin dans les mois qui viennent.
La version précédente du PetShopDNG ne tirait pas parti du framework .NET Remoting. D'un point de vue opérationnel, cela coupait court à tout espoir de distribuer sur des machines différentes la couche de présentation d'une part, et les couches BLL et DAL d'autre part. Et d'un point de vue didactique, cela rendait le PetShopDNG incomplet, puisqu'il n'offrait aucune "bonne pratique" concernant la distribution. Le PetShopDNG 2.0 comble cette lacune, en rendant la couche de services distribuée.
Nous avons donc dû faire des choix de conception, et en particulier choisir QUOI distribuer, et QUE passer par valeur entre les couches distribuées. Les réponses à ces deux questions sont venues de manière complètement naturelle, tant cet aspect a été étudié ces dernières années. Donc sans surprise :
nous avons choisi de rendre la couche de services (la BLL) distribuée : chaque contrôleur de cas d'utilisation est un objet distribué via .NET Remoting, et hérite de ce fait de la classe MarshalByRefObject
et nous nous sommes refusés de rendre les objets métier eux-mêmes distribués, sans quoi l'utilisation du réseau serait devenue abusive, le nombre de proxies aurait énormément augmenté, et le cycle de vie des connexions réseau aurait été allongé. Au contraire, nous avons choisi de passer par un Design Pattern récurrent : le DTO (Data Transfer Object).
Le framework .NET Remoting est très souple : il suffit de le configurer, et en particulier de préciser l'adresse des objets distribués, pour que l'opérateur new instancie un proxy et non l'objet lui-même côté client. Cela nous a aidés à faire en sorte que la couche de présentation ne sache pas si elle manipule des contrôleurs de cas d'utilisation locaux, ou simplement des proxies référençant des contrôleurs qui s'exécutent sur une autre machine. La configuration du framework se fait, bien entendu, dans la classe centrale du PetShopDNG : l'AbstractFactory, dont voici le code source
| namespace PetShopDNG.BLL.DngImpl {using System.Runtime.Remoting; using PetShopDNG.BLL; using PetShopDNG.DAL.DngImpl.Factories; public class DngAbstractFactory : AbstractFactory{ private IAuthenticationController authenticationCtrl; private ISearchController searchCtrl; private IShoppingController shoppingCtrl; private ITestController testCtrl; private IDataMigrationController dataMigrationCtrl; private RemoteFactory remoteFactory; public DngAbstractFactory(){ if (AbstractFactory.IsDistributed){ string remoteAddress = string.Format("{0}://{1}:{2}/", AbstractFactory.Protocol, AbstractFactory.Server, AbstractFactory.PortNumber);
RemotingConfiguration.RegisterWellKnownClientType
RemotingConfiguration.RegisterWellKnownClientType
RemotingConfiguration.RegisterWellKnownClientType
RemotingConfiguration.RegisterWellKnownClientType |