GraphQL in C#: A Practical Guide with Examples

In this post we’re going to see what GraphQL is, how we can integrate it in C#, and whether it’s worth it in a microservices environment. 

 

 

For the code, we'll look at it inside the Distribt Course on GitHub, in this case it has a separate Branch called GraphQL-Example. 

 

 

1 - What is GraphQL?

 

To sum up, GraphQL is an application-level query language that allows users or clients to specify exactly what information they want to receive from a service. 

 

GraphQL lives on the presentation layer of your application and it’s not an interface but the entry point. In other words, it’s an alternative to REST

 

 

1.1 - Example use case of GraphQL

For example, let’s compare REST and GraphQL, since I think most people are more familiar with REST and technology is always easier to understand with examples. 

 

For the demo, let’s look at the code from our distributed systems course, where we have an endpoint to get products by ID:

app.MapGet("product/{productId}", async (int productId, IProductsReadStore readStore)    => await readStore.GetFullProduct(productId));

 

Here we are returning a product, which is built as follows:

public record FullProductResponse(int Id, ProductDetails Details, int Stock, decimal Price);public record ProductDetails(string Name, string Description);

 

But often the description, or even the stock, might not interest us — maybe we only care about the name. This means we’re sending a lot of unnecessary data over the network. 

If you have a small application without much usage, this isn’t generally a problem. But when you have thousands of calls per minute, and the objects you return have dozens of properties, your cloud provider’s bill will show it. 

 

This is where GraphQL shines. It allows us to indicate from the client, or from the system consuming the service, which properties we want returned, and it only sends those. 

NOTE: In REST, this is possible using OData.

 

 

1.2 - GraphQL functionalities

With GraphQL, you can make both queries and modifications, which are called mutations. 

 

I’m not going to talk much about queries, as I think it’s the strongest feature. Personally, I’m not a huge fan of doing mutations with GraphQL, but since it’s possible, we will see how they’re implemented. 

 

In my work experience, I’ve used GraphQL quite a bit when working with Ruby, and honestly, I haven’t used it professionally in .NET. But since it’s a language-agnostic technology, it’s good to explain and understand it.

 

 

1.3 - The GraphQL schema

When you work with GraphQL, each microservice that uses or has GraphQL integrated publishes a schema. This schema is basically the API definition, including the properties, types, and their relationships.

 

When a client makes a request to a service through GraphQL, the schema ensures that the request is 100% correct. 

 

 

2 - Implementing GraphQL in C#

 

Let's get to the practical part where we’ll see how to include GraphQL in our applications. In my case, I’m using the same project as before, and what we have to do is very simple: install the following NuGet packages 

  • GraphQL
  • GraphQL.Server.Transports.AspNetCore
  • GraphQL.SystemTextJson (NOTE: If you use Newtonsoft, there’s a GraphQL.NewtonsoftSerializer)

 

You will need these three packages for every project where you want to integrate GraphQL. In our case, we’ll start with Products.API.Read, so the first thing we’ll see is reading with GraphQL. 

 

 

2.1 - Generate GraphQL Schema in C#

 

The very first step is to generate the schema for what we want to implement or expose via GraphQL. To do this, we’ll create a new class that inherits from GrapQL.Types.Schema:

public class ProductReadSchema : GraphQL.Types.Schema{    public ProductReadSchema()    {    }}

With this, we now have a generated schema. Obviously, it has no functionality yet, but it’s available. So now let's add GraphQL to our API’s configuration. In the program.cs file, we need to add GraphQL to the dependency injection container:

builder.Services.AddGraphQL(x=>{    x.AddSelfActivatingSchema<ProductReadSchema>();    x.AddSystemTextJson();});

And then add the GraphQL middleware:

app.UseGraphQL<ProductReadSchema>();

 

With this, and nothing else, the app will generate the GraphQL Schema. Although it will be completely empty, the next step is to add both queries and mutations. 

 

 

Bonus point:

If you want to see the contents of the schema.graphql file, just like Postman does when integrating, you can do this with the “IntrospectionQuery”. In Postman (or any similar app), create a new HTTP request, set it to POST, and in the body, set it to GraphQL and paste the content from the following link. I’m posting the link since it’s quite long. 

- Link to the file on GitHub.

 

If you want a shorter version, you can create an endpoint that prints the schema:

app.MapGet("graphql-schema", (ProductReadSchema readSchema)    =>{    var schemaPrinter = new SchemaPrinter(readSchema);    return schemaPrinter.Print();});

 

Currently, it returns nothing, but in the next section, we’ll add a query and this would be the result: 

schema {  query: ProductQuery}scalar Decimaltype FullProductResponse {  id: Int!  details: ProductDetailsType  stock: Int!  price: Decimal!}type ProductDetailsType {  name: String!  description: String!}type ProductQuery {  product(id: Int): Product}

As we can see, we see the schema and the types it uses. 

 

 

2.2 - How to query GraphQL?

GraphQL is totally independent of the language you use, and this brings some norms or details to keep in mind.

 

The first and most important is that the types we return from our original REST API DO NOT WORK for GraphQL — we must specify specific types inheriting from ObjectGraphType<T> where T is the inner type. Also, in the constructor you should map from one type to the other using Field.

The good thing is that 99% is done automatically (at least with copilot). Here’s the result of mapping the ProductDetails and FullProductResponse objects:

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

As we see, mapping is as simple as specifying each property with Field(). Name lets you override the name used.

If you don’t specify the name in ProductType, the object in the Schema will be called ProductType instead of FullProductResponse (you can name it whatever you want).

 

NOTE: Try to keep your type names unique throughout your app.

 

Once you have the types, you need to create the Query. Here are a few things to keep in mind: 

 

First, GraphQL objects are Singletons. So, if you inject something scoped, it won’t work. So you must access services via context inside ResolveAsync using GetRequiredService<T>.

 

In my case, I’m injecting the use case of reading a product by Id, and the response type for this use case is FullProductResponse.

 

So we must specify the Field to return, as well as the arguments to receive, and finally get the argument and pass it to the use case: 

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

Now all that’s left is to inject the Query we just created into the schema object:

public class ProductReadSchema : GraphQL.Types.Schema{    public ProductReadSchema(ProductQuery query)    {        Query = query;    }}

 

Now if we run the application, we can see in Postman (or any similar app) that it's reading the schema and we can send queries: 

schema graphql c#

You can see in the query itself which elements you want returned. In this specific case, we are not requesting the description, so it’s not returned.

 

 

2.3 - Creating mutations in GraphQL

 

The process for creating mutations is very similar to queries — you still need your schema and types. In our case, let’s create the mutation for Distribt.Services.Products.Api.Write, which has an endpoint to create products, receiving a CreateProductRequest:

public record CreateProductRequest(ProductDetails Details, int Stock, decimal Price);public record ProductDetails(string Name, string Description);

 

These types must be converted to GraphQL types:

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

 

There are a couple of differences from the previous types; in this case, input types are InputObjectGraphType. And we have an additional output type.

 

Now let’s create the mutation.

 

Just like for queries, you define a Field with a name, the arguments it will accept, and what the mutation will do — in our case, call createProductDetails and insert the product:

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

 

Finally, we need to modify the schema to include the mutation. 

Note that in GraphQL you cannot have schemas with just mutations (not sure why, but you can’t), so you have to create a Query as well — here is the schema:

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

 

If we added both the middleware and GraphQL to the dependency container in program.cs, and the endpoint to view the schema, we can see it generates the following: 

schema {  query: ProductQuery  mutation: ProductMutation}input CreateProductRequest {  details: ProductDetails  stock: Int!  price: Decimal!}scalar Decimaltype FullProductResponse {  url: String!}input ProductDetails {  name: String!  description: String!}type ProductMutation {  createProduct(product: CreateProductRequest): FullProductResponse}type ProductQuery {  info: String}

This means we can go to Postman and try to add a product: 

mutation graphql c#

 

 

2.4 - Other GraphQL functionalities

GraphQL doesn’t stop here — it has more functionalities, and I’ll cover what I consider to be the most important ones: 

 

2.4.1 - Multiple Queries or Mutations in a Single Schema

 

You can only have one query and one mutation per schema, so the usual practice is to have a class (in our case “ProductQuery”) and inside this ProductQuery you can have several different queries or mutations:

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

 

Note: in this case both are the same, but the important thing is how in the schema we have more than one: 

multiple query graphql c#

 

 

2.4.2 - Multiple objects in a single query

With this specific code I don’t have an example, but let’s assume a couple of things. Imagine that in the product query, instead of returning the stock we return an Id. In this case, we would need to query another use case. That can also be done directly from the type using 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);    }}

 

Note: this Resolve also helps when you want to map objects, group them, or change values, etc. 

 

 

2.4.3 - GraphQL Request Pipeline

The request pipeline is an important part of C#; in my opinion, it’s one of the big changes compared to the old .NET Framework.

 

But let’s continue: at this point, you can use middlewares without a problem. What’s more complicated is in filters, but some, such as authentication, are included in the GraphQL library:

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 - HotChocolate library for working with GraphQL

Finally, the request pipeline in GraphQL works similarly to how it does in C#.

If you’re familiar with GraphQL in .NET, you may have wondered why I didn’t use HotChocolate during the implementation.

 

The reason is simple: although HotChocolate includes projections, filters, or middlewares that make life easier for development, for me it’s much more important to understand the mechanism behind it, and the concepts behind an abstraction, before learning an abstraction that may change its logic in the future.  

 

If you want us to explore this library on the channel, leave your comment below! 

 

 

3 - GraphQL federated

 

The beauty of GraphQL is using it with GraphQL federation, which is an architecture pattern that lets you combine multiple GraphQL endpoints into one. When working with microservices, this is a very powerful feature, because any client — whether it’s the website, mobile app, or even third-party apps — will query a single endpoint for all the information they need. 

graphql federated

How it works is simple. 

Each microservice — in our case, the order service and the read/write product services — generates its own GraphQL schema. This schema is called a subgraph and is specific to each microservice. 

 

Now integration comes into play, usually via a gateway — similar to an API Gateway. Basically, it combines the subgraphs from different services into one, which we’ll call the supergraph.

 

Once you have your gateway/SuperGraph set up and get a request, the supergraph knows where to go for the information, allowing for a single request from the client side which returns all needed data even if it’s in multiple microservices. 

 

 

3.1 - Implementing GraphQL federation

 

Implementing a supergraph isn’t as simple as it may seem. When I worked with GraphQL professionally, we used Apollo Federation, which is a third-party service. I’ve looked for alternatives because (in my opinion) it was quite expensive, so here are the alternatives I found:

 

 

Whichever you choose, all require quite a bit of administration and architecture, especially if you do it locally. And they all have a free version, which is enough for a hobby app, but for businesses you’ll need the business model. 

 

If you want me to implement a federated layer with GraphQL and C#, leave your comment below or on the YouTube video, and if there’s demand, I’ll create it.

 

 

4 - Should I use GraphQL?

 

From my point of view (and if you follow me, you’ll know I always make this joke): GraphQL is good as a joke, but for serious development, not so much. I say this tongue-in-cheek, but there are good reasons and I’ll explain.

 

Bear in mind, my opinion isn't the only truth — in fact, there is a situation where GraphQL is really great: eliminating backend-for-frontend (BFF). Maybe I should do a video just about this, but to summarize, backend-for-frontend is when your orders service responds with information from another microservice, e.g., product name, image, etc. 

What you’re doing there is shaping the backend to perform operations needed by the frontend, meaning both services (orders and products) are tightly coupled. 

 

With GraphQL, you don’t need to do that anymore, because if you use a supergraph/federated, that layer is responsible for all the necessary queries. 

 

But this is a double-edged sword, and the reason why I don’t usually recommend it. It also depends on the system — it’s different for a system with 3 calls per minute and one with 50,000.

 

Why? It’s simple. In the previous situation, GraphQL is perfect — it’s the ideal scenario. But what happens with a list? Instead of returning just one order with all the products, we’re returning all orders from the past month — say we have 10,000 orders, and each order has 3 products on average, that’s 30,000 products.

 

Assuming that the order backend only returns the product ID, GraphQL will make one query per product to the product API, that’s 30,000 queries, 30,000 DB connections, causing a huge bottleneck at that moment, and possibly thread pool starvation

 

If you don’t have GraphQL, you have two options: make a query per product (like GraphQL would), or gather all product IDs (maybe 100 or 200 distinct) and make a single backend query, which will connect just once to the database. 

 

Like everything in programming, whether to use GraphQL depends. The important thing is knowing in what scenario it makes sense, why, and what consequences or benefits it brings. For some cases this is amazing, for others not so much. 

 

Aside from that, GraphQL brings other features that can be considered benefits: 

 

GraphQL forces us to use types. In many languages, types are not required (Ruby, for example, is typeless), but it’s very common to have a GraphQL layer which enforces types at least at the client level, and in my opinion this is an advantage — it forces you to have a solid and robust API, which is better in the long run. 

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café