Schema-First Approach with GraphQL.NET in .NET

Introduction

As we discussed earlier, graphQL is a query language for our API, and it provides server-oriented runtime to execute our queries. GraphQL doesn’t rely on any specific programming language or database engine. Besides that, we can develop GraphQL services using most popular programming languages nowadays.

In this article, we will dive into the details of exposing/implementing GraphQL endpoints using GraphQL.NET’s Schema-First approach. This is how our application will look after development. If you want to follow instructions without typing, just download the repo. (GraphQLSchemaFirstApproach application)

GraphQL in .NET: Schema

Of course, we will continue to learn the HotChocolate library, but for the first steps, I think GraphQL.NET is really good for getting started. In the end, it doesn’t matter which library you’re using. The important thing here is to understand how the graphQL service can be implemented. The core concepts are completely the same for all graphQL implementations.

Before starting the implementation, it is very important to understand the basic concepts of graphQL, specially schemas. Here are our articles to follow/learn them.

  1. GraphQL in .Net: Introduction
  2. GraphQL in .NET: Practical Usage
  3. GraphQL in .NET: Pagination
  4. GraphQL in .NET: Schemes

Let's get started

Step 1. Open Visual Studio. Select “Windows Forms App” with .NET 6 for further selection.

GraphQL in .NET: Schema

Step 2. Add 2 richtextbox items with buttons and arrange them as described below.

GraphQL in .NET: Schema

How to Install GraphQL and GraphQL.SystemTextJson?

  • install-package GraphQL
  • install-package GraphQL.SystemTextJson

GraphQL.NET is one of the most popular graphQL implementations in the .Net platform. It supports fresh .NET frameworks and is the most widely used library for graphQL implementation.

The purpose here is not to create a fully layered application using clean code and architectural style applied. So for making things really understandable and for focusing to complete graphQL implementation, all layers are going to live inside Windows Forms Application.(simple monolith)

We ignore database communication and implement a simple in-memory database.

First, let's create domain models:

namespace GraphQLSchemaFirstApproach.Models {
  public record Employee(int Id, string Name, string Fincode, Address Address);
}

namespace GraphQLSchemaFirstApproach.Models {
  public enum CardType {
    Visa,
    Master
  }

  public record Card(int Id, string CardHolder, string CardNumber, CardType CardType, Employee Employee);
}

namespace GraphQLSchemaFirstApproach.Models {
  public record Address(int Id, string City, string Street);
}

The next step is creating a Database folder with the below code.

using GraphQLSchemaFirstApproach.Models;

namespace GraphQLSchemaFirstApproach.Database {
  public class InMemoryDb {
    private readonly List < Employee > _employees;
    private readonly List < Card > _cards;
    public InMemoryDb() {
      _employees = new List < Employee > {
        new Employee(1, "Anar", "123WQER", new Address(1, "Baku", "Khatai 56/78")),
        new Employee(2, "Hasan", "6567RTY", new Address(2, "London", "Oxford Street 34/21")),
        new Employee(3, "Mustafa", "6567RTY", new Address(3, "London", "Carnaby Street 11/11"))
      };

      _cards = new List < Card > () {
        new Card(1, "Anar Mammadov", "3344-5566-7788-8878", CardType.Visa, _employees[0]),
          new Card(2, "Anar Mammadov", "1111-2233-4455-6565", CardType.Master, _employees[0]),
          new Card(3, "Hasan John", "2345-6789-5678-7689", CardType.Visa, _employees[1]),
          new Card(4, "Mustafa McLein", "0001-2345-5678-1234", CardType.Master, _employees[2])
      };
    }

    public Task < List < Employee >> GetEmployeesAsync() {
      return Task.FromResult(_employees);
    }

    public Task < List < Card >> GetCardsAsync() {
      return Task.FromResult(_cards);
    }

    public Task < Employee ? > GetEmployeeByIdAsync(int employeeId) {
      return Task.FromResult(_employees.FirstOrDefault(x => x.Id == employeeId));
    }

    public Task < Card ? > GetCardByIdAsync(int cardId) {
      return Task.FromResult(_cards.FirstOrDefault(x => x.Id == cardId));
    }
  }
}

You can wrap your repositories or service layer functionalities with GrapQL. So, long story short, graphQL will act as a wrapper over your layer. Let's start to wrap our in-memory table.

using GraphQL;
using GraphQLSchemaFirstApproach.Database;
using GraphQLSchemaFirstApproach.Models;

namespace GraphQLSchemaFirstApproach.RootTypes {
  public class Query {

    [GraphQLMetadata("employees")]
    public async Task < IEnumerable < Employee >> GetEmployees() {
      return await new InMemoryDb().GetEmployeesAsync();
    }

    [GraphQLMetadata("cards")]
    public async Task < IEnumerable < Card >> GetCards() {
      return await new InMemoryDb().GetCardsAsync();
    }
  }
}

GraphQL service consists of types, fields, and functions attached to these fields. You can think about it like that: You have classes in C#, and every class is similar to the types in graphQL. Field concept is similar to properties/fields, and functions are equal to methods in C#.

We need to define Query or Mutation classes to glu graphQL to the application we already have.

These classes act as an entry point to graphQL like the Main() method in Console applications. You can define multiple types in graphQL, but only Query and Mutation will act as an entry point.

We use Query to retrieve data ( similar to GET in REST)

We use Mutation to modify data ( Similar to PUT/PATCH/POST in REST)

[GraphQLMetadata] attribute is used to annotate/rename methods to make them accessible from the graphQL using a named string. It helps you to follow the same style ( class name is a noun, and the method name is a verb) but expose created method using the annotated name.

Like we define models in our application as much as required, the same concept is applicable to graphQL. If you want to expose/use any class/record structure from your code via graphQL, you should map the models to the graphQL types. As described above, we need Employee, Card, Address, and CardType to be mapped to graphQL types. To make it possible, create a Schema folder and GraphQLSchema.graphql file with the below content.

type Employee {
  id: Int!
    name: String!
    fincode: String!
    Address: Address!
}

type Address {
  id: Int!
    city: String!
    Street: String!
}

type Card {
  id: Int!
    cardHolder: String!
    cardNumber: String!
    cardType: CardType!
    employee: Employee!
}

enum CardType {
  Visa
  Master
}

PS: You are allowed to rename the file, whatever you want. The extension is also possible to change, but in most cases, it is recommended to store schema files under the .graphql extension file.

After the type definition, we need to think about the Query type definition also in the same file. As mentioned above, Query acts as an entry point and must be defined as a type for GraphQL.

type Query {
  employees: [Employee!] !
    cards: [Card!] !
}

In our Query type, the fields are exactly the same as our [GraphQLMetadata]’s annotation.

To make the file easily accessible, let's create ApplicationPath.cs extension under the Extension folder.

namespace GraphQLSchemaFirstApproach.Extensions {
    public static class ApplicationPath {
      public static string PathTo(string folderName) {
        return Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, @ "..\..\..\", folderName));
          }
        }
      }

To be able to read Schema, let's define a Helper folder and create SchemaLoader.

using GraphQL.Types;
using GraphQLSchemaFirstApproach.Extensions;
using GraphQLSchemaFirstApproach.RootTypes;

namespace GraphQLSchemaFirstApproach.Helpers
{
    public class SchemaLoader
    {
        public const string SchemaFolder = "Schema";
        public const string SchemaExtension = ".graphql";
        public static Schema LoadSchema(string schemaName)
        {
            string pathTograpthQlSchema = Path.Combine(ApplicationPath.PathTo(SchemaFolder), $"{schemaName}{SchemaExtension}");
            string schemaContent = File.ReadAllText(pathTograpthQlSchema);
            return Schema.For(schemaContent, _ => _.Types.Include<Query>());
        }
    }
}

Schema class is a part of GraphQL.NET and provides schema loading functionality with the attached Query.

Using Schema.For() static method, we load our created Schema with a Query entry point. Now this Schema is ready to be executed.

The final touch here is to generate the button_click method. Here is the content of MainForm.

using GraphQL.SystemTextJson;
using GraphQL.Types;
using GraphQLSchemaFirstApproach.Helpers;

namespace GraphQLSchemaFirstApproach {
  public partial class MainForm: Form {
    private Schema _schema;
    public MainForm() {
      InitializeComponent();
      _schema = SchemaLoader.LoadSchema("GraphQLSchema");
    }

    private async void Btn_execute_Click(object sender, EventArgs e) {
      var schemaExecutionResponse = await _schema.ExecuteAsync(_ => _.Query = rcbx_request.Text);
      rcbx_response.Text = schemaExecutionResponse;
    }

  }
}

To be able to execute the query from the richtextbox (in our case name of this component is rcbx_request), Schema has ExecuteAsync() method with Action as an argument. For this action, we set the Query property, and everything is ready. Now you can execute and test your application.

Conclusion

GraphQL is a query language for our API, and it makes our life easy with its interactiveness. There are a lot of libraries out there to start using graphQL in our applications. The above demo was implemented using GraphQL.NET, but the core concepts are the same for all graphQL implementations.


Similar Articles