DEV Community

Cover image for How you can build a Serverless API using GraphQL .Net Core, C# and VS Code
Chris Noring for Microsoft Azure

Posted on • Edited on • Originally published at softchris.github.io

How you can build a Serverless API using GraphQL .Net Core, C# and VS Code

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

This article takes you through how to build a full CRUD API using GraphQL. We end up hosting it in a Serverless function and also show how you can do external HTTP calls to build your API. Whether the data lives here or somewhere else doesn't matter. GraphQL is the front that your users will talk to.

TLDR; This article might be a bit long but it does teach a lot on GraphQL, queries, mutations. It also teaches you how to create a serverless function all in the context of .Net Core, C# and VS Code

In this article we will:

  • Learn how to build a full CRUD GraphQL including queries and mutations
  • Create a Serverless function and Host our GraphQL API in a function
  • Show how you can make an external HTTP call and make that part of our GraphQL API

Resources

 Create a Serverless function app

The first thing we will do is to create a Serverless function. But what is Serverless and why might it be useful in the context of GraphQL? That's really two questions but let's try to answer them in order.

Why Serverless?

Serverless is not about no servers but more about the server is NOT in your basement, anymore. In short, the servers have moved to the Cloud. That's not all, however. Serverless have come to mean other things namely that everything is set up for you. That means you don't need to think about what OS your code runs on or what Web Server runs it, everything is managed. There is more.

There is always more, isn't it? What now a fancy sticker?

It's about cost. Serverless usually means it's cheap.

Why is it cheap?

Well, Serverless code is meant to be run seldom and you are only billed for the time it executes

Ok, how do YOU make money?

Well, the function isn't always there. Whatever resources are needed are allocated when someone/something queries your function.

Doesn't that make it a bit slow like there could be some initial wait when it's scrambling to be created and then respond the query?

You are right. This is something called a cold start. This can be avoided however by either regularly polling our function or using a premium offering that shortens the cold start.

Ok so it's I can either have 100% availability or cheap, pick one :)

I guess so.

Why Serverless with GraphQL?

Ok, we are a bit smarter about the whats and whys of Serverless so why add GraphQL to the mix?

Well, GraphQL has the ability to stitch together data from different APIs so it could act as an API that aggregates data from different sources. So in a sense, it works well in the context of Serverless cause it's not like you need to have any data stored in the function if it's someone else's API.

Ok sounds good, I guess there are no other good reasons the rest is just about the hype that's GraphQL and Serverless right? ;)

...

Prerequisits

To create a Serverless function we first need a function app to put it in. To makes this as easy as possible we will be using VS Code and an Azure Function extension. So our prerequisites are:

  1. Node.js
  2. Visual Studio Code
  3. Azure Functions extension

We can download Node.js from the following page:

https://nodejs.org/en/

You can find Visual Studio code here:

https://code.visualstudio.com/download

As for the extension we need. Search for an extension called Azure Functions. It should look like the below:

Scaffold

Ok, now we should be all setup and ready to create our first Serverless function. As mentioned before, the function needs to live within something called a function app. So we need to create the function app first.

Hit CMD+SHIFT+P or choose View/Command Palette to bring up the command palette:

Now we need to type a command that will help us create a Serverless function app. It's called Azure Functions: Create New Project

Then it's going to ask you for what directory to create the app. The directory you are standing in is the default chosen one, select that.

Next thing it asks about is the language, select C#.

Next up it's asking you about the projects first function and how it should be triggered. Select HttpTrigger.

Now you need to provide a function name. Call it Graphql.

Next question is a namespace. You could really choose anything here but let's call it Function just for the sake of it.

Lastly, it asks about Access Right. There are different options here Anonymous, Function, Admin. We select Anonymous as we want to make our function publically available. The other options mean we would need to provide some kind of key when calling the function.

VS Code should now scaffold all the needed files. It should also ask you to restore all dependencies, to download the libraries. You could also run

dotnet restore

if you should miss clicking on this dialog.

Your project structure should now look like this:

Test it out

Let's ensure that we can run our newly scaffolded function. First, you should have been prompted if you want to add the necessary to be able to debug. You should answer YES here. This will generate a directory .vscode looking like the below:

The tasks.json contains tasks needed to build, clean and run your project. These will assist you with the step we are about to do next Debugging.

To Debug we choose Debug/Start Debugging from the menu. It should compile the code and once done it should look like this:

It's telling us to go to http://localhost:7071/api/Graphql

Let's spin up a browser:

Seems to work alright. :)

It's hitting our function Graphql. Speaking of which, let's have a quick look at the sample code we've been given when we scaffolded the Serverless app:

// Graphql.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Function
{
    public static class Graphql
    {
        [FunctionName("Graphql")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            return name != null
                ? (ActionResult)new OkObjectResult($"Hello, {name}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }
    }
}

Let's not spend to much time understanding everything right now but suffice to say it does its job and is able to work with query parameters:

string name = req.Query["name"];

and the Body, in case you POST a request:

string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;

Next, let's talk GraphQL.

 Adding GraphQL to Serverless

Ok, we established that we wanted to add GraphQL to our Serverless function. First, let's build a GraphQL API. Any GraphQL will contain the same moving parts:

  • A Schema, this will define what we can query for and what custom data types we have
  • A Resolver, a collection of functions that is able to respond to a request and ends up delivering a response

 Adding a GraphQL schema

This will be authored in something called GQL or GraphQL Query Language. Our schema will look like the following:

type Jedi {
  id: ID
  name: String,
  side: String
}

input JediInput {
  name: String
  side: String
  id: ID
}

type Mutation {
  addJedi(input: JediInput): Jedi
  updateJedi(input: JediInput ): Jedi
  removeJedi(id: ID): String
}

type Query {
    jedis: [Jedi]
    jedi(id: ID): Jedi
    hello: String
}

Now that's a lot. Let's explain what we are looking at.

Query

Anything of type Query or Mutation is something we can ask for in our query to our API. The semantic meaning of Query is that you want to fetch data. In this case, we want to fetch a list of jedis using a query syntax like this:

{
  jedis { name side }
}

This is equivalent to writing the following in SQL:

SELECT name, side
FROM jedis;

The other thing we do is supporting a query with a parameter, namely jedi(id: ID): Jedi. We call it like so:

{ 
  jedi(id: 1) { name }
}

This is equivalent to writing the following in SQL:

SELECT name
FROM jedi
WHERE id=1;

Custom types

Everything queryable was defined under type Query. Everything else except for type Mutation are custom types that we define. For example:

type Jedi {
  id: ID
  name: String,
  side: String
}

Mutation

This semantically means we will try to change the data. Looking at the operations we support:

addJedi(input: JediInput): Jedi
updateJedi(input: JediInput ): Jedi
removeJedi(id: ID): String

We can see that we support adding, updating and removal.

To call a mutation we need to type like so:

mutation test { 
  addJedi(input: { 
    name: "JarJar", 
    side: "Dark"  
  }) { name } 
}

Input, complex input for our Mutation

Note the input property input in addJedi(). If we look at the schema we can see that it's of type JediInput which is defined like this:

input JediInput {
  name: String
  side: String
  id: ID
}

The reason we are using the keyword input instead of type is that this is a special case. Special how, you wonder? Well, there are two types of input parameters to a mutation:

  1. Scalars, this is a String, ID, Boolean, etc, also called primitives
  2. Inputs, this is nothing more than a complex data type with many properties on it, example JediInput

so it's definitely possible to define a mutation looking like this:

type Mutation {
  addTodo(todo: String!): String
}

So the million-dollar question is why can't I just use a custom type like Jedi as an input parameter type to my mutation?

The honest answer is I don't know.

Just remember this: If you need an input parameter that is more complex than a scalar, then you need to define it like so:

input MyInputType {
  // my columns
}

Add NuGet package

Ok next step is to set up this schema properly in code. For that we will create a file Server.cs and also install the GraphQL package like so:

dotnet add package GraphQL 

Create a Schema

Now add the following code to Server.cs, like so:

using GraphQL;
using GraphQL.Types;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace Function {
  public class Server 
  {
    private ISchema schema { get; set; }
    public Server() 
    {
      this.schema = Schema.For(@"
          type Jedi {
            id: ID
            name: String,
            side: String
          }

          input JediInput {
            name: String
            side: String
            id: ID
          }

          type Mutation {
            addJedi(input: JediInput): Jedi
            updateJedi(input: JediInput ): Jedi
            removeJedi(id: ID): String
          }

          type Query {
              jedis: [Jedi]
              jedi(id: ID): Jedi
          }
      ", _ =>
      {
        _.Types.Include<Query>();
        _.Types.Include<Mutation>();
      });

    }

    public async Task<string> QueryAsync(string query) 
    {
      var result = await new DocumentExecuter().ExecuteAsync(_ =>
      {
        _.Schema = schema;
        _.Query = query;
      });

      if(result.Errors != null) {
        return result.Errors[0].Message;
      } else {
        return JsonConvert.SerializeObject(result.Data);
      }
    }
  }
}

In the constructor above we set up the Schema by calling Schema.For() with a string representing our schema expressed as GQL. The second argument of that won't compile though, namely this part:

_ =>
  {
    _.Types.Include<Query>();
    _.Types.Include<Mutation>();
  }

Adding resolvers

The reason is it won't compile is that Query and Mutation don't exist yet. They are simply resolver classes answering to query and mutation requests. Let's make it compile by creating first Db.cs and a file Query.cs

Adding in-memory database

// Db.cs

using System.Collections.Generic;
using System.Linq;

namespace Function
{
  public class StarWarsDB
  {
    private static List<Jedi> jedis = new List<Jedi>() {
      new Jedi(){ Id = 1, Name ="Luke", Side="Light"},
      new Jedi(){ Id = 2, Name ="Yoda", Side="Light"},
      new Jedi(){ Id = 3, Name ="Darth Vader", Side="Dark"}
    };
    public static IEnumerable<Jedi> GetJedis()
    {
      return jedis;
    }

    public static Jedi AddJedi(Jedi jedi)
    {
      jedi.Id = jedis.Count + 1;
      jedis.Add(jedi);
      return jedi;
    }

    public static Jedi UpdateJedi(Jedi jedi)
    {
      var toUpdate = jedis.SingleOrDefault(j => j.Id == jedi.Id);
      toUpdate.Name = jedi.Name;
      toUpdate.Side = jedi.Side;
      return toUpdate;
    }

    public static string RemoveJedi(int id)
    {
      var toRemove = jedis.SingleOrDefault(j => j.Id == id);
      jedis.Remove(toRemove);
      return "success";
    }
  }

  public class Jedi
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Side { get; set; }
  }
}

Db.cs is nothing more than a simple in-memory database.

Adding a resolver class to handle all Queries

Next, let's create Query.cs:

// Query.cs

using System.Collections.Generic;
using GraphQL;
using System.Linq;

namespace Function
{
  public class Query
  {
    [GraphQLMetadata("jedis")]
    public IEnumerable<Jedi> GetJedis()
    {
      return StarWarsDB.GetJedis();
    }

    [GraphQLMetadata("jedi")]
    public Jedi GetJedi(int id)
    {
      return StarWarsDB.GetJedis().SingleOrDefault(j => j.Id == id);
    }

    [GraphQLMetadata("hello")]
    public string GetHello()
    {
      return "World";
    }
  }

}

What's interesting about the above is how we map something in our GraphQL schema to a resolver function. For that we use the decorator GraphQLMetadata like so:

[GraphQLMetadata("jedis")]
public IEnumerable<Jedi> GetJedis()
{
  return StarWarsDB.GetJedis();
}

The above tells us that if the user queries for jedis then the function GetJedis() will respond.

Dealing with parameters is almost as easy. It's the same decorator but we just add input parameter like so:

[GraphQLMetadata("jedi")]
public Jedi GetJedi(int id)
{
  return StarWarsDB.GetJedis().SingleOrDefault(j => j.Id == id);
}

Adding a resolver class to handle all Mutation requests

We are almost there but we need the class Mutation.cs and let's add the following content to it:

// Mutation.cs

using GraphQL;

namespace Function {
  public class Mutation
  {
    [GraphQLMetadata("addJedi")]
    public Jedi AddJedi(Jedi  input) 
    {
      return StarWarsDB.AddJedi(input);
    }

    [GraphQLMetadata("updateJedi")]
    public Jedi UpdateJedi(Jedi input)
    {
      return StarWarsDB.AddJedi(input);
    }

    [GraphQLMetadata("removeJedi")]
    public string AddJedi(int id)
    {
      return StarWarsDB.RemoveJedi(id);
    }
  }
}

As you can see it looks very similar to Query.cs, even with parameter handling.

Updating our Serverless function

There's just one piece left of the puzzle, namely our serverless function. It needs to be altered so we can support the user to invoke our function like so:

url?query={ jedis } { name }

Change the code in Graphql.cs to the following:


using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Function
{
    public static class GraphQL
    {
        [FunctionName("GraphQL")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var server = new Server();
            string query = req.Query["query"];
            // string query = "mutation test { addJedi(input: { name: \"JarJar\", side: \"Dark\"  }) { name } }";

            var json = await server.QueryAsync(query);
            return new OkObjectResult(json);
        }
    }
}


Test it all out

To test it we just start debugging by choosing Debug/Start Debugging from the menu and change the URL in the browser to:

http://localhost:7071/api/GraphQL?query={ jedis { name } }

Let's see what the browser says:

Yep, we did it. Serverless function with a GraphQL API.

Bonus - calling other endpoints

Now. One of the great things about GraphQL is that it allows us to call other APIs and thereby GraphQL acts as an aggregation layer. We can easily achieve that by the following steps:

  1. Do the HTTP request, this should do the request to our external API
  2. Add resolver method for our external call
  3. Update our schema with the new type
  4. Test it out

HTTP Request

We can easily do HTTP request in .Net with HttpClient so let's create a class Fetch.cs like so:

// Fetch.cs

using System.Net.Http;
using System.Threading.Tasks;

namespace Function {
  public class Fetch
  {
    private static string BaseUrl = "https://swapi.co/api";
    public static async Task<string> ByUrl(string url)
    {
      using (var client = new HttpClient())
      {
        var json = await client.GetStringAsync(string.Format("{0}/{1}", BaseUrl, url));

        return json;
      }
    }
  }
}

Add resolver method

Now open up Query.cs and add the following method to the Query class:

[GraphQLMetadata("planets")]
  public async Task<List<Planet>> GetPlanet() 
  {
    var planets = await Fetch.ByUrl("planets/");
    var result = JsonConvert.DeserializeObject<Result<List<Planet>>>(planets);
    return result.results;
  }

Additionally, we should be installing a new NuGet package:

dotnet add package Newtonsoft.Json

This is needed so we can convert the JSON response we get into a Poco.

We should also add types Planet and Result to Query.cs, but outside the class definition:

public class Result<T>
{
  public int count { get; set; }
  public T results { get; set; }
}

public class Planet
{
  public string name { get; set; }
}

Update our schema with the new type

We have the schema left to update before we can try it out. So let's open up Schema.cs and make sure it now looks like this:

this.schema = Schema.For(@"
          type Planet {
            name: String
          }

          type Jedi {
            id: ID
            name: String,
            side: String
          }

          input JediInput {
            name: String
            side: String
            id: ID
          }

          type Mutation {
            addJedi(input: JediInput): Jedi
            updateJedi(input: JediInput ): Jedi
            removeJedi(id: ID): String
          }

          type Query {
              jedis: [Jedi]
              jedi(id: ID): Jedi
              hello: String
              planets: [Planet]
          }
      ", _ =>
      {
        _.Types.Include<Query>();
        _.Types.Include<Mutation>();
      });

Above we've added the type Planet:

type Planet {
  name: String
}

We've also added planets to Query:

planets: [Planet]

Testing it out

That should be it, let's test it. Debug/Start Debugging:

http://localhost:7071/api/GraphQL?query={ planets { name } }

There we have it. :)

We've managed to do an external call to an API and make that part of our GraphQL API. As you can see we can intermix data from different sources easily.

Summary

We've managed to create a GraphQL that supports the full CRUD. That is we support both querying for data but also creating, update and delete.

Additionally, we've taken our first steps with Serverless functions.

Finally, we added our GraphQL API to be served by our Serverless function.

As an added bonus we also showed how we could take to external APIs and make them part of our API. That's really powerful stuff.

I hope you enjoyed this article. In out next part we will take a look at how to add GraphQL to a Web API project in .Net Core.

Top comments (8)

Collapse
 
oranintelliflo profile image
Oran Halberthal

Hi,

Nice article, thanks 😀

I played with GraphQL a bit and what really put me off is the schema being defined in a string, so no strong typing, no coherence between the schema and the code etc

It's kind of ok for jedi and Todo examples but a full blown API will become very difficult to manage.

Does anyone know of an implementation that doesn't rely on schema in a string?

Collapse
 
softchris profile image
Chris Noring

well I think the way they solve that is by having extensions like this one marketplace.visualstudio.com/items... so you at least have tooling support. I mean it is strongly typed. You get quite good indications that you are doing it wrong if you start typing in GraphIQL, the UI environment

Collapse
 
jaymeedwards profile image
Jayme Edwards 🍃💻

Great article. I still think we have a long way to go with developer friendliness around these types of APIs. The number of moving parts and concepts that need to be grasped for basic crud borders on intellectual masturbation IMHO.

I’d like to see someone build a framework that generates or abstracts all of this to require way less code.

Great job breaking it down and showing us the state of these technologies!

Collapse
 
softchris profile image
Chris Noring

yea I agree. There definitely exists a lot of great generators for generating code from schemas. That will have to be a separate article :)

Collapse
 
markpieszak profile image
Mark Pieszak • Edited

Wow, another fantastic, in-depth article Chris !
It's always a pleasure to read your work :)

Collapse
 
softchris profile image
Chris Noring

Thank you Mark :) much appreciated

Collapse
 
afsharm profile image
Afshar

Thanks for the article. I am excited about GraphQL. We are currently using OData heavily. I hope GraphQL can be a good alternative or at least be a complementary for OData.

Collapse
 
softchris profile image
Chris Noring

thank you, Jeffrey. Really appreciate that :)