用C#给我写一个GraphQL服务器

不知何故,我休息了几天,然后将GraphQL服务器绘制到了我们的Docsvision平台上。 下面我将告诉你它是如何进行的。


海报-按要求


什么是Docsvision平台


Docsvision平台包括许多用于构建工作流系统的工具,但其关键组件类似于ORM。 有一个元数据编辑器,您可以在其中描述卡字段的结构。 可能会有结构部分,集合部分和树部分,此外,它们可以嵌套, 通常,一切都很复杂 。 数据库是由元数据生成的,然后您可以通过某些C#API使用它。 一言以蔽之-构建GraphQL服务器的理想选择。


有哪些选择


老实说,没有太多选择,而且都是这样。 我设法只找到两个库:



UPD:他们在评论中建议仍然存在Hotchocolate


在README上,起初我喜欢第二个,甚至开始使用它。 但是他很快发现她的API太差了,她无法应付生成元数据方案的任务。 但是,它似乎已经被放弃(一年前的最后一次提交)。


graphql-dotnet API相当灵活,但同时graphql-dotnet文档记录,混乱graphql-dotnet直观。 要了解如何使用它,我必须看一下源代码...的确,我使用0.16版,而现在的最后一个是0.17.3 ,并且已经发布了7个beta版本2.0 。 因此,如果材料过时,我感到抱歉。


我还必须提到,库带有未签名的程序集。 为了在带有签名程序集的ASP.NET应用程序中使用它们,我必须从源代码手动重建它们。


GraphQL服务器结构


如果您不熟悉GraphQL,则可以尝试github Explorer 。 一个小秘密-您可以按Ctrl +空格键来自动完成。 客户端部分无非就是GraphiQL ,可以轻松地将其固定到服务器上。 只需获取index.html ,从npm包中添加脚本,然后将graphQLFetcher函数中的url更改为服务器的地址-就可以了。


考虑一个简单的查询:


 query { viewer { login, company } } 

在这里,我们看到一组字段-查看器,登录名,公司。 像GraphQL后端一样,我们的任务是在服务器上构建一些“方案”,在其中将处理所有这些字段。 实际上,我们只需要使用字段描述创建适当的服务对象结构,并定义用于计算值的回调函数。


该方案可以基于C#类自动生成,但是我们将经历核心问题-我们将全力以赴。 但这不是因为我是一个时髦的人,仅生成基于元数据的架构是graphql-dotnet中的非标准脚本,官方文档不支持该脚本。 因此,我们在她的肠道内的无证部位挖了一点。


创建了方案之后,我们仍然可以通过任何方便的方式(无论GET,POST,SignalR,TCP ...如何都无关紧要)将请求字符串(和参数)从客户端传递到服务器,并随方案一起提供其引擎。 引擎将吐出一个对象,结果是我们将其转换为JSON并将其返回给客户端。 对我来说看起来像这样:


  //  ,        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); 

您可以注意sessionContext 。 这是特定于Docsvision的对象,可通过该对象访问平台。 在创建方案时,我们总是在特定的环境下工作,但以后会更多。


电路生成


一切都以感人的方式开始:


 Schema schema = new Schema(); 

不幸的是,这就是简单代码的结尾。 为了向方案添加字段,我们需要:


  1. 描述其类型-创建一个ObjectGraphType,StringGraphType,BooleanGraphType,IdGraphType,IntGraphType,DateGraphType或FloatGraphType对象。
  2. 描述字段本身(名称,处理程序)-创建一个GraphQL.Types.FieldType对象

让我们尝试描述我上面引用的简单请求。 在请求中,我们有一个字段-查看者。 要将其添加到查询中,必须首先描述其类型。 它的类型很简单-一个具有两个字符串字段的对象-登录名和公司。 我们描述登录字段:


 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; } } 

我们以相同的方式创建companyField对象-非常好,我们准备描述查看器字段的类型。


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

有一个类型,现在我们可以描述查看器字段本身:


 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; } } 

好了,最后一点,将我们的字段添加到查询类型:


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

就是这样,我们的计划已经准备就绪。


收集,分页,参数处理


如果该字段返回的不是一个对象,而是一个集合,则需要显式指定此对象。 为此,只需将属性类型包装在ListGraphType类的实例中。 假设查看者返回了一个集合,我们可以简单地写成这样:


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

因此,在MyViewerResolver解析器中,有必要返回列表。


当出现收集字段时,立即进行分页很重要。 这里没有现成的机制,所有操作都通过参数完成。 您可能会注意到在上面的示例中使用参数的示例(cardDocument具有id参数)。 让我们向查看器添加这样的参数:


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

然后,您可以像这样在解析器中获取参数值:


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

GraphQL的类型如此之大,以至于Guid当然无法解析。 好吧,对我们来说并不难。


Docsvision卡申请


因此,在针对Docsvision平台的GrapqhQL的实现中,我只需遍历元数据代码( sessionContext.Session.CardManager.CardTypes ),对于所有卡及其部分,我都会使用相应的解析器自动创建此类对象。 结果是这样的:


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

这里cardDocument是卡的类型,mainInfo是其中的部分的名称,name和instanceID是该部分中的字段。 卡,部分和字段的相应解析器按以下方式使用CardManager API:


  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]; } } 

当然,在这里您只能通过ID申请卡,但是以相同的方式生成方案以访问高级报告,服务和其他任何内容都很容易。 使用此API,您只需编写适当的JavaScript即可从Docsvision数据库获取任何数据-编写自己的脚本和扩展名非常方便。


结论


使用.NET中的GrapqhQL,事情并不容易。 有一个稍微活跃的库,没有可靠的供应商,前途未卜,API不稳定且奇怪,未知在负载下的行为方式和稳定性。 但是,我们拥有的东西似乎可以正常运行,但是文档中的缺陷和其余缺陷已被源代码的开放性所抵消。


我在本文中描述的是一个越来越不公开的API,我通过键入和研究源代码对其进行了探索。 只是库的作者并不认为有人需要自动生成电路-恩,您能做什么,这是开源的。


到目前为止,所有这些都是单独编写的,到目前为止,只不过是一个原型。 在标准Docsvision软件包中,这可能会出现,但是何时-仍然很难说。 但是,如果您希望直接从JavaScrpit访问Docsvision数据库而不编写服务器扩展的想法,请编写。 合作伙伴的兴趣越高,我们将对此进行更多的关注。

Source: https://habr.com/ru/post/zh-CN416501/


All Articles