Escreva-me um servidor GraphQL em C #

De alguma forma, tive alguns dias de folga e esbocei o servidor GraphQL para nossa plataforma Docsvision. Abaixo vou lhe dizer como foi.


Cartaz - a pedido


O que é a plataforma Docsvision


A plataforma Docsvision inclui muitas ferramentas diferentes para criar sistemas de fluxo de trabalho, mas seu principal componente é algo como ORM. Há um editor de metadados no qual você pode descrever a estrutura dos campos do cartão. Pode haver seções estruturais, de coleção e de árvore que, além disso, podem ser aninhadas, em geral, tudo é complicado . Um banco de dados é gerado por metadados e, em seguida, você pode trabalhar com ele por meio de alguma API C #. Em uma palavra - uma opção ideal para construir um servidor GraphQL.


Quais são as opções


Honestamente, não há muitas opções e são mais ou menos. Consegui encontrar apenas duas bibliotecas:



UPD: nos comentários, sugeriram que ainda existe Hotchocolate .


No README, no começo eu gostei do segundo, e até comecei a fazer algo com ele. Mas ele logo descobriu que a API dela era muito ruim e ela não conseguia lidar com a tarefa de gerar um esquema de metadados. No entanto, parece que já foi abandonado (o último commit há um ano).


A API graphql-dotnet é bastante flexível, mas ao mesmo tempo é terrivelmente documentada, confusa e pouco intuitiva. Para entender como trabalhar com isso, tive que examinar o código-fonte ... É verdade que trabalhei com a versão 0.16 , enquanto agora o último é 0.17.3 e 7 versões beta 2.0 já foram lançadas. Então, desculpe se o material está um pouco desatualizado.


Devo também mencionar que as bibliotecas vêm com assemblies não assinados. Eu tive que reconstruí-los a partir da fonte manualmente para usá-los em nosso aplicativo ASP.NET com assemblies assinados.


Estrutura do servidor GraphQL


Se você não estiver familiarizado com o GraphQL, tente o github explorer . Um pequeno segredo - você pode pressionar Ctrl + espaço para obter a conclusão automática. A parte do cliente nada mais é do que o GraphiQL , que pode ser facilmente parafusado no servidor. Basta pegar index.html , adicionar scripts do pacote npm e alterar a URL da função graphQLFetcher para o endereço do seu servidor - isso é tudo, você pode jogar.


Considere uma consulta simples:


 query { viewer { login, company } } 

Aqui vemos um conjunto de campos - visualizador, nele login, empresa. Nossa tarefa, como o back-end do GraphQL, é criar no servidor algum "esquema" no qual todos esses campos serão processados. De fato, precisamos apenas criar a estrutura apropriada dos objetos de serviço com a descrição dos campos e definir funções de retorno de chamada para calcular os valores.


O esquema pode ser gerado automaticamente com base nas classes C # , mas passaremos pelo hardcore - faremos tudo com nossas mãos. Mas isso não é porque eu sou um cara ousado, apenas gerar um esquema baseado em metadados é um script não padrão no graphql-dotnet que não é suportado pela documentação oficial. Então, cavamos um pouco em seu intestino, em uma área não documentada.


Depois de criar o esquema, resta entregar a string de solicitação (e os parâmetros) do cliente para o servidor de qualquer maneira conveniente (não importa como GET, POST, SignalR, TCP ...) e alimentar seu mecanismo junto com o esquema. O mecanismo cuspirá um objeto com um resultado que transformamos em JSON e o devolvemos ao cliente. Parecia assim para mim:


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

Você pode prestar atenção ao sessionContext . Este é o nosso objeto específico da Docsvision através do qual a plataforma é acessada. Ao criar um esquema, sempre trabalhamos com um contexto específico, mas mais sobre isso posteriormente.


Geração de circuitos


Tudo começa de uma maneira emocionante:


 Schema schema = new Schema(); 

Infelizmente, é aqui que o código simples termina. Para adicionar um campo ao esquema, precisamos:


  1. Descreva seu tipo - crie um objeto ObjectGraphType, StringGraphType, BooleanGraphType, IdGraphType, IntGraphType, DateGraphType ou FloatGraphType.
  2. Descreva o próprio campo (nome, manipulador) - crie um objeto GraphQL.Types.FieldType

Vamos tentar descrever esse pedido simples que citei acima. Na solicitação, temos um visualizador de campo. Para adicioná-lo a uma consulta, você deve primeiro descrever seu tipo. Seu tipo é simples - um objeto, com dois campos de string - login e empresa. Nós descrevemos o campo de login:


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

Criamos o objeto companyField da mesma maneira - excelente, estamos prontos para descrever o tipo do campo do visualizador.


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

Existe um tipo, agora podemos descrever o próprio campo do visualizador:


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

Bem, e no toque final, adicione nosso campo ao tipo de consulta:


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

Isso é tudo, nosso esquema está pronto.


Coleções, paginação, processamento de parâmetros


Se o campo retornar não um objeto, mas uma coleção, será necessário especificar isso explicitamente. Para fazer isso, basta agrupar o tipo de propriedade em uma instância da classe ListGraphType. Suponha que se um visualizador retornasse uma coleção, simplesmente escreveríamos o seguinte:


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

Assim, no resolvedor MyViewerResolver, seria necessário retornar a lista.


Quando os campos de coleção são exibidos, é importante cuidar da paginação imediatamente. Não há mecanismo pronto aqui, tudo é feito através dos parâmetros . Você pode observar um exemplo de uso do parâmetro no exemplo acima (cardDocument possui um parâmetro de identificação). Vamos adicionar esse parâmetro ao visualizador:


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

Então você pode obter o valor do parâmetro no resolvedor assim:


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

O GraphQL é tão digitado que Guid, é claro, não conseguiu analisar. Bem, tudo bem, não é difícil para nós.


Solicitação de cartão da Docsvision


Na implementação do GrapqhQL para a plataforma Docsvision, simplesmente sessionContext.Session.CardManager.CardTypes o código de metadados ( sessionContext.Session.CardManager.CardTypes ) e, para todos os cartões e suas seções, crio automaticamente esses objetos com os resolvedores correspondentes. O resultado é algo como isto:


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

Aqui cardDocument é o tipo de cartão, mainInfo é o nome da seção, name e instanceID são os campos da seção. Os resolvedores correspondentes para o cartão, seção e campo usam a API do CardManager da seguinte maneira:


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

Obviamente, aqui você só pode solicitar cartões por ID, mas é fácil gerar um esquema da mesma maneira para acessar relatórios, serviços avançados e qualquer outra coisa. Com essa API, você pode obter quaisquer dados do banco de dados do Docsvision simplesmente escrevendo o JavaScript apropriado - é muito conveniente para escrever seus próprios scripts e extensões.


Conclusão


Com o GrapqhQL no .NET, as coisas não são fáceis. Existe uma biblioteca um tanto animada, sem um fornecedor confiável e com um futuro incompreensível, uma API instável e estranha, desconhecida em como ela se comportará sob carga e quão estável é. Mas temos o que temos, parece funcionar, mas as falhas na documentação e o restante são compensadas pela abertura do código-fonte.


O que descrevi neste artigo é uma API cada vez mais não documentada, que eu explorei digitando e estudando a fonte. Só que os autores da biblioteca não pensaram que alguém precisaria gerar o circuito automaticamente - bem, o que você pode fazer, isso é de código aberto.


Foi escrito tudo isso no fim de semana e, por si só, até agora não é mais que um protótipo. No pacote padrão do Docsvision, é provável que isso apareça, mas quando - ainda é difícil dizer. No entanto, se você gosta da idéia de acessar o banco de dados do Docsvision diretamente do JavaScrpit sem gravar extensões do servidor, escreva. Quanto maior o interesse dos parceiros, mais atenção dedicaremos a isso.

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


All Articles