Écrivez-moi un serveur GraphQL en C #

D'une manière ou d'une autre, j'ai eu quelques jours de congé et j'ai dessiné le serveur GraphQL sur notre plate-forme Docsvision. Ci-dessous, je vais vous dire comment cela s'est passé.


Affiche - à la demande


Qu'est-ce que la plateforme Docsvision


La plate-forme Docsvision comprend de nombreux outils différents pour créer des systèmes de workflow, mais son composant clé est quelque chose comme ORM. Il existe un éditeur de métadonnées dans lequel vous pouvez décrire la structure des champs de la carte. Il peut y avoir des sections structurelles, de collection et d'arbre, qui, de plus, peuvent être imbriquées, en général, tout est compliqué . Une base de données est générée par des métadonnées, puis vous pouvez l'utiliser avec une API C #. En un mot - une option idéale pour construire un serveur GraphQL.


Quelles sont les options


Honnêtement, il n'y a pas beaucoup d'options et elles sont telles. J'ai réussi à trouver seulement deux bibliothèques:



UPD: dans les commentaires, ils ont suggéré qu'il y avait encore Hotchocolate .


Sur README, au début, j'ai aimé le second, et j'ai même commencé à faire quelque chose avec. Mais il a rapidement découvert que son API était trop pauvre et qu'elle ne pouvait pas faire face à la tâche de générer un schéma de métadonnées. Cependant, il semble déjà avoir été abandonné (le dernier commit il y a un an).


L'API graphql-dotnet est assez flexible, mais en même temps, elle est terriblement documentée, déroutante et peu intuitive. Pour comprendre comment travailler avec, j'ai dû regarder le code source ... Vrai, j'ai travaillé avec la version 0.16 , alors que la dernière est 0.17.3 , et 7 versions beta 2.0 ont déjà été publiées. Je suis donc désolé si le matériel est un peu dépassé.


Je dois également mentionner que les bibliothèques sont livrées avec des assemblages non signés. J'ai dû les reconstruire manuellement à partir de la source afin de les utiliser dans notre application ASP.NET avec des assemblys signés.


Structure du serveur GraphQL


Si vous n'êtes pas familier avec GraphQL, vous pouvez essayer l' explorateur github . Un petit secret - vous pouvez appuyer sur Ctrl + espace pour obtenir l'auto-complétion. La partie client n'y est rien de plus que GraphiQL , qui peut être facilement vissé sur votre serveur. Prenez simplement index.html , ajoutez des scripts à partir du package npm et changez l'url dans la fonction graphQLFetcher à l'adresse de votre serveur - c'est tout, vous pouvez jouer.


Considérez une requête simple:


 query { viewer { login, company } } 

Ici, nous voyons un ensemble de champs - visionneuse, connexion, entreprise. Notre tâche, comme le backend GraphQL, est de construire sur le serveur un "schéma" dans lequel tous ces champs seront traités. En fait, il suffit de créer la structure appropriée des objets de service avec la description des champs et de définir des fonctions de rappel pour le calcul des valeurs.


Le schéma peut être généré automatiquement sur la base des classes C # , mais nous passerons par le hardcore - nous ferons tout avec nos mains. Mais ce n'est pas parce que je suis un gars fringant, la simple génération d'un schéma basé sur les métadonnées est un script non standard dans graphql-dotnet qui n'est pas pris en charge par la documentation officielle. Donc, nous creusons un peu dans son intestin, dans une zone non documentée.


Après avoir créé le schéma, il nous reste à livrer la chaîne de requête (et les paramètres) du client au serveur de toute manière pratique (peu importe comment GET, POST, SignalR, TCP ...), et à alimenter son moteur avec le schéma. Le moteur crachera un objet avec un résultat que nous transformerons en JSON et le retournerons au client. Ça ressemblait à ça pour moi:


  //  ,        var schema = GraphQlService.GetCardsSchema(sessionContext); //    (  ) var executer = new DocumentExecuter(); //   ,  var dict = await executer.ExecuteAsync(schema, sessionContext, request.Query, request.MethodName).ConfigureAwait(false); // -   :) if (dict.Errors != null && dict.Errors.Count > 0) { throw new InvalidOperationException(dict.Errors.First().Message); } //    return Json(dict.Data); 

Vous pouvez prêter attention à sessionContext . Il s'agit de notre objet spécifique à Docsvision à travers lequel la plateforme est accessible. Lors de la création d'un schéma, nous travaillons toujours avec un contexte particulier, mais plus à ce sujet plus tard.


Génération de circuits


Tout commence de manière touchante:


 Schema schema = new Schema(); 

Malheureusement, c'est là que se termine le code simple. Pour ajouter un champ au schéma, nous avons besoin de:


  1. Décrivez son type - créez un objet ObjectGraphType, StringGraphType, BooleanGraphType, IdGraphType, IntGraphType, DateGraphType ou FloatGraphType.
  2. Décrivez le champ lui-même (nom, gestionnaire) - créez un objet GraphQL.Types.FieldType

Essayons de décrire cette simple demande que j'ai citée ci-dessus. Dans la demande, nous avons un visualiseur de champ. Pour l'ajouter à une requête, vous devez d'abord décrire son type. Son type est simple - un objet, avec deux champs de chaîne - login et company. Nous décrivons le champ de connexion:


 var loginField = new GraphQL.Types.FieldType(); loginField.Name = "login"; loginField.ResolvedType = new StringGraphType(); loginField.Type = typeof(string); loginField.Resolver = new MyViewerLoginResolver(); // ... class MyViewerLoginResolver : GraphQL.Resolvers.IFieldResolver { public object Resolve(ResolveFieldContext context) { // ,       -   UserInfo //      viewer return (context.Source as UserInfo).AccountName; } } 

Nous créons l'objet companyField de la même manière - excellent, nous sommes prêts à décrire le type du champ de visualisation.


 ObjectGraphType<UserInfo> viewerType = new ObjectGraphType<UserInfo>(); viewerType.Name = "Viewer"; viewerType.AddField(loginField); viewerType.AddField(companyField); 

Il existe un type, maintenant nous pouvons décrire le champ du visualiseur lui-même:


 var viewerField = new GraphQL.Types.FieldType(); viewerField.Name = "viewer"; viewerField.ResolvedType = viewerType; viewerField.Type = typeof(UserInfo); viewerField.Resolver = new MyViewerResolver(); // ... class MyViewerResolver : GraphQL.Resolvers.IFieldResolver { public object Resolve(ResolveFieldContext context) { //     sessionContext   ? // ,         (login  company) return (context.Source as SessionContext).UserInfo; } } 

Eh bien, et la touche finale, ajoutez notre champ au type de requête:


 var queryType = new ObjectGraphType(); queryType.AddField(viewerField); schema.Query = queryType; 

C'est tout, notre plan est prêt.


Collections, pagination, traitement des paramètres


Si le champ ne renvoie pas un objet, mais une collection, vous devez le spécifier explicitement. Pour ce faire, enveloppez simplement le type de propriété dans une instance de la classe ListGraphType. Supposons que si un spectateur retourne une collection, nous écrirons simplement ceci:


 //  ( ) viewerField.ResolvedType = viewerType; //  () viewerField.ResolvedType = new ListGraphType(viewerType); 

En conséquence, dans le résolveur MyViewerResolver, il serait alors nécessaire de renvoyer la liste.


Lorsque des champs de collecte apparaissent, il est important de prendre soin de la pagination immédiatement. Il n'y a pas de mécanisme prêt à l'emploi ici, tout se fait à travers les paramètres . Vous pouvez remarquer un exemple d'utilisation du paramètre dans l'exemple ci-dessus (cardDocument a un paramètre id). Ajoutons un tel paramètre au visualiseur:


 var idArgument = new QueryArgument(typeof(IdGraphType)); idArgument.Name = "id"; idArgument.ResolvedType = new IdGraphType(); idArgument.DefaultValue = Guid.Empty; viewerField.Arguments = new QueryArguments(idArgument); 

Ensuite, vous pouvez obtenir la valeur du paramètre dans le résolveur comme ceci:


 public object Resolve(ResolveFieldContext context) { var idArgStr = context.Arguments?["id"].ToString() ?? Guid.Empty.ToString(); var idArg = Guid.Parse(idArgStr); 

GraphQL est si typé que Guid, bien sûr, n'a pas pu analyser. Et bien, ce n’est pas difficile pour nous.


Demande de carte Docsvision


Dans l'implémentation de GrapqhQL pour la plateforme Docsvision, je passe donc simplement par le code de métadonnées ( sessionContext.Session.CardManager.CardTypes ), et pour toutes les cartes et leurs sections, je crée automatiquement ces objets avec les résolveurs correspondants. Le résultat est quelque chose comme ceci:


 query { cardDocument(id: "{AF652E55-7BCF-E711-8308-54A05079B7BF}") { mainInfo { name instanceID } } } 

Ici cardDocument est le type de carte, mainInfo est le nom de la section, nom et instanceID sont les champs de la section. Les résolveurs correspondants pour la carte, la section et le champ utilisent l'API CardManager comme suit:


  class CardDataResolver : GraphQL.Resolvers.IFieldResolver { public object Resolve(ResolveFieldContext context) { var sessionContext = (context.Source as SessionContext); var idArg = Guid.Parse(context.Arguments?["id"].ToString() ?? Guid.Empty.ToString()); return sessionContext.Session.CardManager.GetCardData(idArg); } } class SectionResolver : GraphQL.Resolvers.IFieldResolver { CardSection section; public SectionFieldResolver(CardSection section) { this.section = section; } public object Resolve(ResolveFieldContext context) { var idArg = Guid.Parse(context.Arguments?["id"].ToString() ?? Guid.Empty.ToString()); var skipArg = (int?)context.Arguments?["skip"] ?? 0; var takeArg = (int?)context.Arguments?["take"] ?? 15; var sectionData = (context.Source as CardData).Sections[section.Id]; return idArg == Guid.Empty ? sectionData.GetAllRows().Skip(skipArg).Take(takeArg) : new List<RowData> { sectionData.GetRow(idArg) }; } } class RowFieldResolver : GraphQL.Resolvers.IFieldResolver { Field field; public RowFieldResolver(Field field) { this.field = field; } public object Resolve(ResolveFieldContext context) { return (context.Source as RowData)[field.Alias]; } } 

Bien sûr, ici, vous ne pouvez demander des cartes que par identifiant, mais il est facile de générer un schéma de la même manière pour accéder aux rapports avancés, aux services et à toute autre chose. Avec cette API, vous pouvez obtenir toutes les données de la base de données Docsvision en écrivant simplement le code JavaScript approprié - c'est très pratique pour écrire vos propres scripts et extensions.


Conclusion


Avec GrapqhQL dans .NET, les choses ne sont pas faciles. Il existe une bibliothèque quelque peu animée, sans fournisseur fiable et avec un avenir incompréhensible, une API instable et étrange, inconnue quant à son comportement sous charge et à sa stabilité. Mais nous avons ce que nous avons, cela semble fonctionner, mais les failles dans la documentation et le reste sont compensées par l'ouverture du code source.


Ce que j'ai décrit dans cet article est une API de plus en plus non documentée, que j'ai explorée en tapant et en étudiant la source. C'est juste que les auteurs de la bibliothèque ne pensaient pas que quelqu'un aurait besoin de générer le circuit automatiquement - eh bien, que pouvez-vous faire, c'est open source.


Il a été écrit tout cela pendant le week-end, et à lui seul, jusqu'à présent, pas plus qu'un prototype. Dans le package Docsvision standard, cela est susceptible d'apparaître, mais quand - c'est encore difficile à dire. Cependant, si vous aimez l'idée d'accéder à la base de données Docsvision directement à partir de JavaScrpit sans écrire d'extensions de serveur, écrivez. Plus l'intérêt des partenaires est élevé, plus nous y accorderons d'attention.

Source: https://habr.com/ru/post/fr416501/


All Articles