En este post vamos a ver que es GraphQL, como lo podemos integrar en C# y si nos merece la pena en un entorno de microservicios.
Índice
Para el código lo vamos a ver dentro del Curso de Distribt en GitHub, en este caso tiene un Branch separado que se llama GrapQL-Example.
1 - Qué es GraphQL?
Podemos resumir en que graphql es un lenguaje de consultas a nivel de aplicación que permite a los usuarios o clientes especificar exactamente qué información quieren recibir de un servicio.
GraphQL reside en la capa de presentación de tu aplicación y no es una interfaz sino el punto de entrada. En otras palabras, es una alternativa a REST.
1.1 - Ejemplo de uso de GraphQL
Para poner un ejemplo, vamos a comprar REST y GraphQL, el motivo es que pienso que la gran mayoría de personas están más acostumbradas a REST y las tecnologías siempre se entienden mejor con un ejemplo.
Para la presentación vamos a ir al código de nuestro curso de sistemas distribuidos, donde tenemos un endpoint de leer productos por id:
app.MapGet("product/{productId}", async (int productId, IProductsReadStore readStore)
=> await readStore.GetFullProduct(productId));
Aquí estamos devolviendo un producto el cual está construido de la siguiente forma:
public record FullProductResponse(int Id, ProductDetails Details, int Stock, decimal Price);
public record ProductDetails(string Name, string Description);
Pero en muchas ocasiones la descripción, o incluso el stock no nos interesa, lo único que nos interesa es el nombre, lo que implica que vamos a estar enviando muchos más datos por la red de los necesarios.
Si tienes una aplicación pequeña que no se usa mucho, esto no suele ser un problema, pero cuando tienes miles de llamadas por minuto y los objetos que devuelves tienen decenas de propiedades, la factura del proveedor de servicios se hace notar.
Aquí es donde entra GraphQL, nos permite indicar, desde la parte del cliente, o sistema que está consumiendo este servicio, que propiedades queremos que nos devuelva, y el resto no las devuelve.
NOTA: En REST, esto es posible con OData.
1.2 - Funcionalidades de GraphQL
Cuando utilizamos GraphQL, podemos hacer tanto consultas, como modificaciones, a las cuales se les llama mutaciones.
De la parte de consulta no voy a hablar mucho, porque me parece su mayor fuerte. Pero personalmente no soy muy fan de hacer mutaciones con GraphQL, pero cómo es posible, veremos cómo se implementan.
En el entorno laboral he trabajado con GraphQL bastante cuando trabajaba con Ruby y la verdad es que en .NET no he trabajado nunca de forma profesional, pero como es una tecnología agnostica al lenguaje que utilices, creo que va muy bien explicarla y entenderla.
1.3 - El esquema de GraphQL
Cuando trabajas con GraphQL cada microservicio que va a utilizar, o tiene integrado, GraphQL va a publicar un esquema, este esquema es básicamente la definición de la API, tanto de las propiedades como de los tipos o las relaciones entre ellas.
Cuando un cliente hace una llamada a un servicio a través de graphQL, el schema va a validar que la llamada sea correcta al 100%.
2 - Implementar GraphQL en C#
Vamos a pasar a la parte práctica donde vamos a ver como incluir GraphQL en nuestras aplicaciones, en mi caso estoy utilizando el mismo proyecto de antes, y lo que tenemos que hacer es muy sencillo, debemos instalar los siguientes paquetes de nuget
GraphQL
GraphQL.Server.Transports.AspNetCore
GraphQL.SystemTextJson
(NOTA: si usas Newtonsoft, hay un GraphQL.NewtonsoftSerializer)
Estos tres paquetes los vas a necesitar en todos los proyectos que quieras integrar GraphQL, en nuestro caso concreto vamos a empezar por Products.API.Read
, por lo tanto lo primero que vamos a ver son lecturas con GraphQL.
2.1 - Generar GraphQL Schema en C#
El primer paso de todos es generar el esquema de lo que queremos implementar o habilitar para que pueda ser consultado utilizando GraphQL, para ello crearemos una nueva clase que herede de GrapQL.Types.Schema
:
public class ProductReadSchema : GraphQL.Types.Schema
{
public ProductReadSchema()
{
}
}
Con esto ya tenemos el esquema generado, obviamente no tiene ninguna funcionalidad, pero está disponible. Por lo tanto, vamos a incluir GraphQL a la configuración de la API, para ello en el fichero program.cs
debemos añadir al contenedor de dependencias GraphQL:
builder.Services.AddGraphQL(x=>
{
x.AddSelfActivatingSchema<ProductReadSchema>();
x.AddSystemTextJson();
});
Y posteriormente añadir el middleware de graphQL:
app.UseGraphQL<ProductReadSchema>();
Con esto, sin nada más, la app ya nos genera el GraphQL Schema. Aunque estará completamente vacío, lo que debemos hacer ahora es añadir tanto queries, como mutaciones.
Punto adicional:
Si quieres ver el contenido del fichero schema.graphql
, igual que hace postman a la hora de integrarlo puedes hacerlo con la “IntrospectionQuery”, en postman (o cualquier app similar) creas una nueva request HTTP, que sea POST y en el body, seleccionas que sea Graphql y pegas el contenido del siguiente enlace. Lo pongo en un enlace porque es muy largo.
- Enlace al fichero en GitHub.
Si quieres una versión más corta, puedes crear un endpoint el cual imprima el schema:
app.MapGet("graphql-schema", (ProductReadSchema readSchema)
=>
{
var schemaPrinter = new SchemaPrinter(readSchema);
return schemaPrinter.Print();
});
En el estado actual no devuelve nada, pero en el punto siguiente vamos a incluir una query, y este sería el resultado:
schema {
query: ProductQuery
}
scalar Decimal
type FullProductResponse {
id: Int!
details: ProductDetailsType
stock: Int!
price: Decimal!
}
type ProductDetailsType {
name: String!
description: String!
}
type ProductQuery {
product(id: Int): Product
}
Como podemos ver, vemos el esquema, y los tipos que utiliza.
2.2 - Cómo realizar una consulta en GraphQL?
GraphQL es completamente independiente del lenguaje que vayamos a utilizar, y esto trae consigo una serie de normas, o detalles a tener en consideración.
El primero y más importante es que los tipos que devolvemos en nuestra API rest original, NO NOS SIRVEN para GraphQL , debemos especificar tipos específicos que heredaran del objeto ObjectGraphType<T>
donde T
es el tipo interno. Además en el constructor deberemos mapear de un tipo al otro utilizando Field
.
La parte buena es que el 99% se hace de forma automática, por lo menos con copilot, este sería el resultado de mapear los objetos ProductDetails
y FullProductResponse
:
public class ProductType : ObjectGraphType<FullProductResponse>
{
public ProductType()
{
Name = "Product";
Field(x => x.Id);
Field(x => x.Details, type: typeof(ProductDetailsType));
Field(x => x.Stock);
Field(x => x.Price);
}
}
public class ProductDetailsType : ObjectGraphType<ProductDetails>
{
public ProductDetailsType()
{
Field(x => x.Name);
Field(x => x.Description);
}
}
Como vemos, hacer el mapeo es tan sencillo como especificar cada propiedad con Field()
y Name
, lo que hace es sobreescribir el nombre que va a utilizar.
Si no especificaremos el nombre en ProductType
, el objeto del Schema en vez de llamarse FullProductResponse
se llamaría ProductType
(Le puedes poner el nombre que quieras).
NOTA: Intenta tener nombres de tipos únicos a lo largo de tu aplicación.
Una vez tienes los tipos, debemos crear la Query, Aquí debemos tener varias cosas en mente.
La primera es que los objetos de graphql son Singleton por lo tanto si estamos inyectando algo scoped, no va a funcionar . Por lo tanto debemos acceder a los servicios a través del context dentro de ResolveAsync
, donde podemos hacer un GetRequiredService<T>
.
En mi caso, estoy inyectando el caso de uso de leer un producto por Id. y el tipo de respuesta de este caso de uso es FullProductResponse
.
Por lo que debemos especificar el Field
que vamos a devolver, así como los argumentos que vamos a recibir y finalmente leer dicho argumento y pasarlo al caso de uso:
public class ProductQuery : ObjectGraphType<object>
{
public ProductQuery()
{
Field<ProductType>("FullProductResponse")
.Description("Get a full product by ID")
.Arguments(new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }))
.ResolveAsync(async ctx =>
{
var id = ctx.GetArgument<int>("id");
IGetProductById getById = ctx.RequestServices!.GetRequiredService<IGetProductById>();
return await getById.Execute(id);
});
}
}
Ahora únicamente nos queda inyectar la Query que acabamos de crear al objeto del schema:
public class ProductReadSchema : GraphQL.Types.Schema
{
public ProductReadSchema(ProductQuery query)
{
Query = query;
}
}
Y ahora si corremos la aplicación, podemos ver en Postman (o cualquier app similar), que nos está leyendo el esquema y podemos hacer consultas:
Se puede observar como es en la propia consulta, cuales son los elementos que queremos que nos devuelvan. En este caso en concreto, no estamos solicitando description por lo que no nos lo devuelve.
2.3 - Crear mutaciones con GraphQL
El proceso de crear mutaciones es muy similar al de las consultas, igual necesitamos nuestro schema y los tipos, en nuestro caso vamos a crear la mutación para Distribt.Services.Products.Api.Write
, que tiene un endpoint para crear productos el cual recibe un CreateProductRequest
:
public record CreateProductRequest(ProductDetails Details, int Stock, decimal Price);
public record ProductDetails(string Name, string Description);
Estos tipos los tenemos que convertir en tipos de GraphQL:
public class ProductDetailsType : InputObjectGraphType<ProductDetails>
{
public ProductDetailsType()
{
Name = "ProductDetails";
Field(x => x.Name);
Field(x => x.Description);
}
}
public class CreateProductRequestType : InputObjectGraphType<CreateProductRequest>
{
public CreateProductRequestType()
{
Name = "CreateProductRequest";
Field(x => x.Details, type: typeof(ProductDetailsType));
Field(x => x.Stock);
Field(x => x.Price);
}
}
public class CreateProductResponseType : ObjectGraphType<CreateProductResponse>
{
public CreateProductResponseType()
{
Name = "FullProductResponse";
Field(x => x.Url);
}
}
Hay un par de diferencias con respecto a los tipos anteriores, y es que en este caso los tipos de entrada son InputObjectGraphType
. Y tenemos un tipo adicional para el de salida.
Y ahora pasamos a crear la mutación.
Igual que en la consulta, debemos epsecificar un Field
, con su nombre, los argumentos que va a recibir y que es lo que dicha mutación va a hacer, en nuestro caso llamar a createProductDetails e insertar el producto:
public class ProductMutation : ObjectGraphType<object>
{
public ProductMutation()
{
Field<CreateProductResponseType>("CreateProduct")
.Description("Create a product in the system")
.Arguments(new QueryArguments(new QueryArgument<CreateProductRequestType> { Name = "product" }))
.ResolveAsync(async ctx =>
{
var product = ctx.GetArgument<CreateProductRequest>("product");
ICreateProductDetails createProduct = ctx.RequestServices!.GetRequiredService<ICreateProductDetails>();
return await createProduct.Execute(product);
});
}
}
Finalmente debemos modificar el esquema para que contenga la mutación.
OJO, en GraphQL no podemos tener esquemas con solo mutaciones, no entiendo muy bien porqué, pero no se puede, así que debemos crear una Query si o si, este sería el esquema:
public class ProductWriteSchema : GraphQL.Types.Schema
{
public ProductWriteSchema(ProductMutation mutation, ProductQuery query)
{
Mutation = mutation;
Query = query;
}
}
public class ProductQuery : ObjectGraphType<object>
{
public ProductQuery()
{
Field<StringGraphType>("info")
.Resolve(_ => "Hello World");
}
}
Y si hemos añadido en el program.cs
tanto el middleware, como GraphQL al contenedor de dependencias y el endpoint para ver el esquema, podemos observar que nos genera lo siguiente:
schema {
query: ProductQuery
mutation: ProductMutation
}
input CreateProductRequest {
details: ProductDetails
stock: Int!
price: Decimal!
}
scalar Decimal
type FullProductResponse {
url: String!
}
input ProductDetails {
name: String!
description: String!
}
type ProductMutation {
createProduct(product: CreateProductRequest): FullProductResponse
}
type ProductQuery {
info: String
}
Lo que significa que podemos ir a Postman y probar a añadir un producto:
2.4 - Otras funcionalidades de GraphQL
GraphQL no acaba aquí sino que tiene más funcionalidades, y las que yo considero más importantes las vamos a ver:
2.4.1 - Múltiples Consultas o mutations en un único Schema
Solo puedes tener una query y una mutación por esquema, lo que se suele hacer es tener una clase, en nuestro caso “ProductQuery” y dentro de este product query puedes tener varias consultas o mutaciones diferentes:
public class ProductQuery : ObjectGraphType<object>
{
public ProductQuery()
{
Field<ProductType>("FullProductResponse")
.Description("Get a full product by ID")
.Arguments(new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }))
.ResolveAsync(async ctx =>
{
var id = ctx.GetArgument<int>("id");
IGetProductById getById = ctx.RequestServices!.GetRequiredService<IGetProductById>();
return await getById.Execute(id);
});
Field<ProductType>("FullProductResponseOp2")
.Description("Get a full product by ID")
.Arguments(new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }))
.ResolveAsync(async ctx =>
{
var id = ctx.GetArgument<int>("id");
IGetProductById getById = ctx.RequestServices!.GetRequiredService<IGetProductById>();
return await getById.Execute(id);
});
}
}
Nota: en este caso es lo mismo, pero lo importante es ver como en el esquema tenemos más de una:
2.4.2 - Múltiples objetos en una sola consulta
Con este código en concreto no tengo un ejemplo, pero vamos a asumir un par de cosas, imaginemos que en el caso de la query de producto, en vez de devolver el stock estamos devolviendo un Id. en este caso tendríamos que consultar otro caso de uso. Eso también se puede hacer desde el tipo directamente si utilizamos Resolve:
public class ProductType : ObjectGraphType<FullProductResponse>
{
public ProductType()
{
Name = "FullProductResponse";
Field(x => x.Id);
Field(x => x.Details, type: typeof(ProductDetailsType));
Field(x => "itemsInStock")
.Resolve(ctx=> 👈
ctx.RequestServices!.GetRequiredService<IProductStock>()
.GetStock(ctx.Source.StockID));
Field(x => x.Price);
}
}
Nota: este Resolve también nos puede servir para mapear objetos cuando queremos agruparlos o cambiarles valores, etc.
2.4.3 - Request Pipeline de graphQL
La request pipeline es una parte muy importante dentro de C#, es más, en mi opinión es uno de los grandes cambios con respecto al antiguo .NET Framework.
Pero bueno sigamos, llegados a este punto podemos utilizar middlewares, sin problema y donde lo tenemos más complicado es en los filtros, pero algunos, como el de la autenticación están incluidos en la librería de GraphQL:
public class ProductQuery : ObjectGraphType<object>
{
public ProductQuery()
{
Field<ProductType>("FullProductResponse")
.Description("Get a full product by ID")
.Arguments(new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }))
.ResolveAsync(async ctx =>
{
var id = ctx.GetArgument<int>("id");
IGetProductById getById = ctx.RequestServices!.GetRequiredService<IGetProductById>();
return await getById.Execute(id);
})
.Authorize() 👈
.AuthorizeWithPolicy("policy1") 👈
.AuthorizeWithRoles("role"); 👈
}
}
2.4.4 - Librería HotChocolate para trabajar con GraphQL
Finalmente la request pipeline de GraphQL funciona de una forma similar a lo que funciona C#
Si estás algo familiarizado con GraphQL en .NET te habrás preguntado por qué no he utilizado HotChocolate durante la implementación.
El motivo es muy sencillo, pese a que HotChocolate incluye proyecciones, filtros o middlewares que facilitan la vida del desarrollo, para mi es mucho más importante entender el mecanismo que funciona por detrás, y saber los conceptos detrás de una abstracción, antes que entender una abstracción la cual es más posible que cambie de funcionamiento.
Si quieres que exploremos esta librería en el canal, ¡deja tu comentario abajo!
3 - Graphql federated
La gracia de GraphQL es usarlo con GraphQL federation que es un patrón de arquitectura que te permite combinar múltiples endpoints GraphQL en uno solo, y cuando trabajas con microservicios es una funcionalidad muy poderosa, ya que cualquier cliente, ya sea la página web, la app móvil o incluso apps de terceros, van a consultar un único endpoint y ahí consultará toda la información que necesite.
La forma en la que funciona es simple.
Cada microservicio, en nuestro caso el de pedidos, y los de lectura y escritura de productos generan un esquema GraphQL, a este esquema lo llamaremos subgraph y es específico de cada uno de los microservicios.
Ahora llega la integración, normalmente se hace a través de una gateway, la cual es similar a una API Gateway. Básicamente combina los subgraphs de los diferentes servicios en uno solo, a lo que llamaremos supergraph.
Una vez tienes su gateway/SuperGraph montado y recibimos una llamada, el supergraph sabe donde tiene que ir a buscar la información, permitiendo así una única llamada desde el lado del cliente y devolviendo toda la información necesaria, aunque esté en múltiples microservicios.
3.1 - Implementar GraphQL federation
Implementar un supergraph no es tan sencillo como parece, cuando trabajé con GraphQL de forma profesional nosotros usábamos Apollo Federation, que es un servicio de terceros, y he estado buscando alternativas porque, en mi opinión era bastante caro, de todas formas, estas son las alternativas que he encontrado:
- Apollo GraphQL https://www.apollographql.com/
- Wundergraph OOS https://wundergraph.com/
- Hive https://the-guild.dev/graphql/hive
- Inigo https://inigo.io/
- Graphbase https://grafbase.com/
Elijas la que elijas, todas tienen bastante administración y arquitectura que configurar, especialmente si lo haces en local. Y todas tienen una versión gratuita que es suficiente para una aplicación de hobby, pero para empresas necesitarás el modelo de empresas.
Si quieres que implemente una federated layer con GraphQL y C# Deja tu comentario abajo o en el vídeo de YouTube y si tiene pedida, lo crearé.
4 - Debo usar Graphql?
Desde mi punto de vista y si me sigues, siempre menciono que GraphQL como broma está bien, pero para desarrollo serio pues no. La verdad es que lo digo muy así a la torera pero lo que digo viene con fundamento y lo voy a explicar.
Ojo, no quiere decir que mi opinión sea la única verdadera, de hecho hay en una situación donde GraphQL es muy bueno, y es eliminando backend for front end, quizá debería hacer un vídeo únicamente hablando sobre este tema, pero resumiendo, back end for front end es cuando, desde tu servicio de pedidos u orders estas respondiendo información de otro microservicio, por ejemplo el nombre del producto, la imagen, etc.
Lo que haces ahí es acomodar el backend para que realice operaciones que necesita el front end. Por lo tanto, ambos servicios, tanto el de pedidos como el de productos están fuertemente acoplados.
Con la llegada de GraphQL no necesitas hacer esto, porque si utilizas un supergraph/federated es esa capa la que hace todas las consultas necesarias.
Lo que es un arma de doble filo y el motivo por el que yo, de normal, no lo recomiendo, también depende del sistema, no es lo mismo un sistema con 3 llamadas por minuto que uno con 50 mil.
¿Por qué? Muy sencillo, en el caso anterior GraphQL nos viene perfecto, de hecho es el escenario ideal, pero, ¿qué pasa en una lista? En vez de devolver un solo pedido con todos los productos, estamos devolviendo los pedidos del último mes, pongamos que tenemos 10 mil pedidos y en los pedidos 3 productos de media lo que hace 30 mil productos.
Asumiendo que en el backend de los pedidos únicamente devuelves el ID del producto, GraphQL hará una consulta por producto a la api de productos, esto quiere decir que hará 30 mil consultas, lo que implica 30 mil conexiones a la bbdd y un cuello de botella tremendo en ese momento en particular, además de posiblemente thread pool starvation.
En cambio, si no tienes GraphQL tienes dos opciones, una es hacer una llamada por cada prodcuto, igual que lo haría GraphQL o, coges todos los IDs de los productos, que quizá son 100 o 200 distintos y haces una única consulta al BackEnd, el cual conectará una única vez con la base de datos.
Así que como todo en la programación, saber si usar graphQL o no, depende. Lo importante es saber en qué escenarios es bueno aplicarlo, por qué y qué consecuencias o beneficios puede traer. Ya que para algunos casos esta funcionalidad es impresionante pero para otros no lo es tanto.
Al margen de eso nos trae otras características que sí podemos considerar como beneficios.
GraphQL nos fuerza a tener tipos, hay en muchos lenguajes donde los tipos no son necesarios, en Ruby por ejemplo no hay tipos, pero es muy común tener una capa de GraphQL lo que fuerza a tener tipos por lo menos a nivel de cliente y en mi opinión es una ventaja, ya que te fuerza a tener una API sólida y robusta, lo que a la larga es mejor.