前言

GraphQL 是一種查詢語言,看起來類似 JSON,而它的查詢結果是 JSON 型別。最初由 Facebook 提出,後來獨立為一個基金會。它的好處在於它的靈活度,對於前端來說,如果要稍微改變從 API 上取得的資料,只要調整一下 GraphQL 的查詢就可以了。 不過也由於它的靈活度,對於開發者而言,也會增加複雜度。

什麼是 GraphQL

要了解 GraphQL 不是件容易的事,所以筆者提供網路上神人做好的範例,大家可以先玩玩看:https://www.graphqlbin.com/v2/mqZgc5

來看一下範例網址一開始的語法:

query {
  me {
    name
    friends {
      name
    }
    posts {
      title
      likeGivers {
        name
      }
    }
  }
}

這段查詢的結果最後會是一個 JSON 結果:

{
  "data": {
    "me": {
      "name": "Fong",
      "friends": [
        {
          "name": "Kevin"
        },
        {
          "name": "Mary"
        }
      ],
      "posts": [
        {
          "title": "Hello World!!",
          "likeGivers": [
            {
              "name": "Fong"
            },
            {
              "name": "Mary"
            }
          ]
        },
        {
          "title": "Love U Too",
          "likeGivers": [
            {
              "name": "Fong"
            },
            {
              "name": "Kevin"
            },
            {
              "name": "Mary"
            }
          ]
        }
      ]
    }
  }
}

一開始看到查詢跟結果肯定會疑惑,但看了以下的 Schema 後就會清楚了:

directive @cacheControl(
  maxAge: Int
  scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE
enum CacheControlScope {
  PUBLIC
  PRIVATE
}

enum HeightUnit {
  METRE
  CENTIMETRE
  FOOT
}

type Post {
  id: ID!
  author: User
  title: String
  content: String
  createdAt: String
  likeGivers: [User]
}

type Query {
  hello: String
  me: User
  users: [User]
  user(name: String!): User
}

scalar Upload

type User {
  id: ID!
  email: String!
  name: String
  age: Int
  height(unit: HeightUnit = CENTIMETRE): Float
  weight(unit: WeightUnit = KILOGRAM): Float
  friends: [User]
  posts: [Post]
  birthDay: String
}

enum WeightUnit {
  KILOGRAM
  GRAM
  POUND
}

當我們看到

type Query {
  hello: String
  me: User
  users: [User]
  user(name: String!): User
}

這段時,就能理解前面的查詢,回傳的是一個 User 物件,而 User 的宣告長這樣:

type User {
  id: ID!
  email: String!
  name: String
  age: Int
  height(unit: HeightUnit = CENTIMETRE): Float
  weight(unit: WeightUnit = KILOGRAM): Float
  friends: [User]
  posts: [Post]
  birthDay: String
}

其實前面的查詢欄位有點多,而且欄位又剛好也是類別型態,所以簡化一下:

query {
  me {
    name
    email
  }
}

結果如下:

{
  "data": {
    "me": {
      "name": "Fong",
      "email": "fong@test.com"
    }
  }
}

這也說明了,對前端來說,API 的 endpoint 只有一個,查詢時,也可以依據需求取得需要的資料欄位,不必因為 API 的限制硬是取得一整包的資料。

建立 ASP.NET Core 專案並實作 GraphQL

看完以上的範例,這邊就簡單利用 ASP.NET Core 來示範如何做一個簡單的 GraphQL API。

註:這邊只講解 GraphQL 相關的程式碼,資料庫相關的會略過。

首先,安裝相關套件:

  • GraphQL
  • GraphQL.Server.Transports.AspNetCore
  • GraphQL.Server.Ui.Playground

之後準備下列 GraphQL 相關的物件:

  • ObjectGraphType (包含 data model 跟 query)
  • Schema

首先,這邊示範的資料庫類別如下:

public class Employee
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int CompanyId { get; set; }
}

public class Company
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public string Desc { get; set; }
}

然後是對應的 Graph Type:

public EmployeeType()
{
    Field(x => x.Id, type: typeof(IdGraphType)).Description("...");
    Field(x => x.Name).Description("...");
    Field(x => x.Age, type: typeof(IntGraphType)).Description("...");
    Field(x => x.CompanyId, type: typeof(IntGraphType)).Description("...");
}
    
public class CompanyType : ObjectGraphType<Company>
{
    public CompanyType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("...");
        Field(x => x.Name).Description("...");
        Field(x => x.Desc).Description("...");
    }
}

接下來是 Query 跟 Schema:

public class MainQuery: ObjectGraphType
{
    public MainQuery(IEmployeeRepository employeeRepository, ICompanyRepository companyRepository)
    {
        Name = "Query";
        Field<ListGraphType<EmployeeType>>(
            "employees",
            resolve: context => employeeRepository.GetAll()
        );
        Field<ListGraphType<CompanyType>>(
            "companies",
            resolve: context => companyRepository.GetAll()
        );
    }
}

public class MainSchema : Schema
{
    public MainSchema(IDependencyResolver resolver) : base(resolver)
    {
        Query = resolver.Resolve<MainQuery>();
    }

}

之後在 Startup.cs 加入相關設定:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddGraphQL(o => { o.ExposeExceptions = true; })
        .AddGraphTypes(ServiceLifetime.Scoped);

    // If using IIS:
    services.Configure<IISServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });

    ...
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseGraphQL<MainSchema>();
    app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions());

    ...
}

之後開啟 https://localhost:[port]/graphql,輸入下列查詢語法就可以得到 JSON 資料:

query {
  employees
  {
    name
  }
}

參考資料