Escríbeme un servidor GraphQL en C #

De alguna manera, tuve un par de días libres y dibujé el servidor GraphQL en nuestra plataforma Docsvision. A continuación te contaré cómo te fue.


Cartel - a instancias


¿Qué es la plataforma Docsvision?


La plataforma Docsvision incluye muchas herramientas diferentes para construir sistemas de flujo de trabajo, pero su componente clave es algo así como ORM. Hay un editor de metadatos en el que puede describir la estructura de los campos de la tarjeta. Puede haber secciones estructurales, de colección y de árbol, que, además, se pueden anidar, en general, todo es complicado . Los metadatos generan una base de datos y luego puede trabajar con ella a través de alguna API de C #. En una palabra: una opción ideal para construir un servidor GraphQL.


Cuales son las opciones


Honestamente, no hay muchas opciones y son más o menos. Logré encontrar solo dos bibliotecas:



UPD: en los comentarios sugirieron que todavía hay Hotchocolate .


En README, al principio me gustó el segundo, e incluso comencé a hacer algo con él. Pero pronto descubrió que su API era demasiado pobre y que no podía hacer frente a la tarea de generar un esquema de metadatos. Sin embargo, parece que ya ha sido abandonado (el último compromiso hace un año).


La API graphql-dotnet es bastante flexible, pero al mismo tiempo está terriblemente documentada, confusa y poco intuitiva. Para entender cómo trabajar con él, tuve que mirar el código fuente ... Es cierto, trabajé con la versión 0.16 , mientras que ahora la última es 0.17.3 , y ya se han lanzado 7 versiones beta 2.0 . Así que lo siento si el material está un poco desactualizado.


También debo mencionar que las bibliotecas vienen con ensambles sin firmar. Tuve que reconstruirlos desde la fuente manualmente para poder usarlos en nuestra aplicación ASP.NET con ensamblados firmados.


Estructura del servidor GraphQL


Si no está familiarizado con GraphQL, puede probar github explorer . Un pequeño secreto: puede presionar Ctrl + espacio para completar automáticamente. La parte del cliente no es más que GraphiQL , que se puede atornillar fácilmente a su servidor. Simplemente tome index.html , agregue scripts del paquete npm y cambie la url en la función graphQLFetcher a la dirección de su servidor; eso es todo, puede jugar.


Considere una consulta simple:


 query { viewer { login, company } } 

Aquí vemos un conjunto de campos: visor, inicio de sesión, empresa. Nuestra tarea, como el backend GraphQL, es construir en el servidor algún "esquema" en el que se procesen todos estos campos. De hecho, solo necesitamos crear la estructura adecuada de los objetos de servicio con la descripción de los campos y definir funciones de devolución de llamada para calcular los valores.


El esquema se puede generar automáticamente en función de las clases de C # , pero pasaremos por el hardcore: haremos todo con nuestras manos. Pero esto no es porque soy un tipo apuesto, solo generar un esquema basado en metadatos es un script no estándar en graphql-dotnet que no es compatible con la documentación oficial. Entonces, cavamos un poco en su intestino, en un área indocumentada.


Una vez creado el esquema, nos queda entregar la cadena de solicitud (y los parámetros) desde el cliente al servidor de cualquier manera conveniente (no importa cómo GET, POST, SignalR, TCP ...), y alimentar su motor junto con el esquema. El motor escupirá un objeto con un resultado que convertimos en JSON y lo devolvemos al cliente. Se veía así para mí:


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

Puede prestar atención a sessionContext . Este es nuestro objeto específico de Docsvision a través del cual se accede a la plataforma. Al crear un esquema, siempre trabajamos con un contexto particular, pero más sobre eso más adelante.


Generación de circuito


Todo comienza de una manera conmovedora:


 Schema schema = new Schema(); 

Desafortunadamente, aquí es donde termina el código simple. Para agregar un campo al esquema, necesitamos:


  1. Describa su tipo: cree un objeto ObjectGraphType, StringGraphType, BooleanGraphType, IdGraphType, IntGraphType, DateGraphType o FloatGraphType.
  2. Describa el campo en sí (nombre, controlador): cree un objeto GraphQL.Types.FieldType

Tratemos de describir esa simple solicitud que cité anteriormente. En la solicitud, tenemos un campo: el visor. Para agregarlo a una consulta, primero debe describir su tipo. Su tipo es simple: un objeto con dos campos de cadena: inicio de sesión y empresa. Describimos el campo de inicio de sesión:


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

Creamos el objeto companyField de la misma manera: excelente, estamos listos para describir el tipo de campo del visor.


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

Hay un tipo, ahora podemos describir el campo del visor en sí:


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

Bueno, y el toque final, agregue nuestro campo al tipo de consulta:


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

Eso es todo, nuestro esquema está listo.


Colecciones, paginación, procesamiento de parámetros.


Si el campo no devuelve un solo objeto, sino una colección, debe especificarlo explícitamente. Para hacer esto, simplemente ajuste el tipo de propiedad en una instancia de la clase ListGraphType. Supongamos que un espectador devuelve una colección, simplemente escribiríamos esto:


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

En consecuencia, en el solucionador MyViewerResolver, sería necesario devolver la lista.


Cuando aparecen los campos de recopilación, es importante ocuparse de la paginación de inmediato. No hay un mecanismo listo aquí, todo se hace a través de los parámetros . Puede observar un ejemplo de uso del parámetro en el ejemplo anterior (cardDocument tiene un parámetro de identificación). Agreguemos dicho parámetro al espectador:


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

Luego puede obtener el valor del parámetro en el resolutor de la siguiente manera:


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

GraphQL está tan escrito que Guid, por supuesto, no pudo analizar. Oh bueno, no es difícil para nosotros.


Solicitud de tarjeta Docsvision


En la implementación de GrapqhQL para la plataforma Docsvision, simplemente sessionContext.Session.CardManager.CardTypes código de metadatos ( sessionContext.Session.CardManager.CardTypes ), y para todas las tarjetas y sus secciones, automáticamente creo tales objetos con los resolvers correspondientes. El resultado es algo como esto:


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

Aquí cardDocument es el tipo de tarjeta, mainInfo es el nombre de la sección que contiene, name y instanceID son los campos de la sección. Los resolvers correspondientes para la tarjeta, la sección y el campo utilizan la API CardManager de la siguiente manera:


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

Por supuesto, aquí solo puede solicitar tarjetas por identificación, pero es fácil generar un esquema de la misma manera para acceder a informes avanzados, servicios y cualquier otra cosa. Con esta API, puede obtener cualquier información de la base de datos de Docsvision simplemente escribiendo el JavaScript apropiado; es muy conveniente para escribir sus propios scripts y extensiones.


Conclusión


Con GrapqhQL en .NET, las cosas no son fáciles. Hay una biblioteca algo animada, sin un proveedor confiable y con un futuro incomprensible, una API inestable y extraña, desconocida en cómo se comportará bajo carga y qué tan estable es. Pero tenemos lo que tenemos, parece funcionar, pero los defectos en la documentación y el resto se ven compensados ​​por la apertura del código fuente.


Lo que describí en este artículo es una API cada vez más indocumentada, que exploré escribiendo y estudiando la fuente. Es solo que los autores de la biblioteca no pensaron que alguien necesitaría generar el circuito automáticamente; bueno, qué puedes hacer, esto es de código abierto.


Fue escrito todo esto durante el fin de semana, y por sí solo, hasta ahora no es más que un prototipo. En el paquete estándar de Docsvision, es probable que esto aparezca, pero cuándo, aún es difícil de decir. Sin embargo, si le gusta la idea de acceder a la base de datos de Docsvision directamente desde JavaScrpit sin escribir extensiones de servidor, escriba. Cuanto mayor sea el interés de los socios, más atención le dedicaremos a esto.

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


All Articles