C#のGraphQLサーバー構築 【HotChocolate 】

# GraphQL 使ってみたい～
けどまだ実務では使う機会がないので予習しておく :man_tone3:

- C#でGraphQLサーバーを構築
- ライブラリには`HotChocolate`を採用 :chocolate_bar:
- [ワークショップ](https://github.com/ChilliCream/graphql-workshop)のページがあるため、実装して体感する

## 実行環境
- Windows10
- .NET8
- VSCode
- .NET 8.0 Runtime (v8.0.2) - Windows x64

## 1. プロジェクト作成
まずターミナルでdotnetコマンドからプロジェクト作成
```bash
dotnet new sln -n ConferencePlanner
dotnet new web -n GraphQL
dotnet sln add GraphQL
```
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/3125e8ca-70bb-70ad-4588-3bb2e5c8598e.png">

`Data`フォルダを用意して、`Speaker.cs`クラスを作成
```bash
mkdir GraphQL/Data
touch Data/Speaker.cs
```

```Data/Speaker.cs
using System.ComponentModel.DataAnnotations;

namespace ConferencePlanner.GraphQL.Data
{
    public class Speaker
    {
        public int Id { get; set; }

        [Required]
        [StringLength(200)]
        public string? Name { get; set; }

        [StringLength(4000)]
        public string? Bio { get; set; }

        [StringLength(1000)]
        public virtual string? WebSite { get; set; }
    }
}
```

パッケージを追加
```bash
dotnet add GraphQL package Microsoft.EntityFrameworkCore.Sqlite --version 8.0.2
```
追加に成功するとプロジェクトファイルに以下の項目が追加される
```GraphQL.csproj
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
```

`Entity Framework`でDB接続するため`ApplicationDbContext.cs`クラスを`Data`フォルダに作成
```bash
touch Data/ApplicationDbContext.cs
```

```Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;

namespace ConferencePlanner.GraphQL.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Speaker> Speakers { get; set; }
    }
}
```

エントリーポイントの`Program.cs`に`ApplicationDbContext`をサービスとして登録する
```Program.cs
using ConferencePlanner.GraphQL.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddDbContext<ApplicationDbContext>(options =>
	options.UseSqlite("Data Source=conferences.db"));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

```

`ORM`パッケージを追加
```bash
dotnet add GraphQL package Microsoft.EntityFrameworkCore.Tools --version 8.0.2
dotnet add GraphQL package Microsoft.EntityFrameworkCore.Design --version 8.0.2
```

`dotnet-ef`のインストール
DBのマイグレーションの作成や適用、DBスキーマのスキャフォールディングなどをコマンドラインから実行できる
```bash
dotnet new tool-manifest
dotnet tool install dotnet-ef --version 8.0.2 --local
```

```.config/dotnet-tools.json
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "dotnet-ef": {
      "version": "8.0.2",
      "commands": [
        "dotnet-ef"
      ]
    }
  }
}
```

ビルド
```bash
dotnet build GraphQL
```

マイグレーション実行
```bash
dotnet ef migrations add Initial --project GraphQL
dotnet ef database update --project GraphQL
```

`GraphQL`パッケージを追加
```bash
dotnet add GraphQL package HotChocolate.AspNetCore --version 13.9.0
```
### Query
リゾルバーとなる`Query.cs`クラスを追加
リソースを取得する（RESTでいうGET）
```Query.cs
using System.Linq;
using HotChocolate;
using ConferencePlanner.GraphQL.Data;

namespace ConferencePlanner.GraphQL
{
    public class Query
    {
        public IQueryable<Speaker> GetSpeakers([Service] ApplicationDbContext context) =>
            context.Speakers;
    }
}
```

`Program.cs`にサービスを追加
依存性注入に`GraphQL`スキーマを登録し、`Query`タイプを登録している
```Program.cs
using ConferencePlanner.GraphQL.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddDbContext<ApplicationDbContext>(options =>
	options.UseSqlite("Data Source=conferences.db"));

// 追加
builder.Services
	.AddGraphQLServer()
    .AddQueryType<Query>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();
```

`GraphQL`ミドルウェアを構成して、サーバーが`GraphQL`リクエストを実行する方法を認識できるようにする
```Program.cs
using ConferencePlanner.GraphQL.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// サービスの登録
builder.Services.AddDbContext<ApplicationDbContext>(options =>
	options.UseSqlite("Data Source=conferences.db"));

builder.Services
	.AddGraphQLServer()
    .AddQueryType<Query>();

var app = builder.Build();

// ルーティングのミドルウェアを追加
app.UseRouting();

// エンドポイントのミドルウェアを設定
app.UseEndpoints(endpoints =>
{
    endpoints.MapGraphQL();
});

app.MapGet("/", () => "Hello World!");

app.Run();
```

サーバーを起動
```bash
dotnet run --project GraphQL
```

サーバーの起動に成功すると以下のような画面になる
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/4f81f3b3-265f-8ceb-6d13-28af4426a271.png">

サーバーが起動した後、`http://localhost:5042/graphql`にアクセスすると[Banana Cake Pop](https://chillicream.com/docs/bananacakepop/v2)IDEが開く:banana:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/327c3e09-8862-d03a-d19c-af377fcbfa28.png)

`Brose Schema`ボタンをクリック後、`Speaker`フィールドをクリックしてスキーマの返り値の型を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/4ad2354d-819b-1526-0ab4-ca05bed5c2af.png)

### Mutation
リソースを変更する（RESTでいうPOST,PUT,DELETE）
以下、ChatGPTによる解説
```
GraphQLにおけるMutationは、データを変更する操作を表します。
これにはデータの追加、更新、削除などが含まれます。
簡単に言うと、Mutationはデータベースやサーバーに何か変更を加えたいときに使います。

Mutationは大きく3つの部分から構成されています。

Mutationの定義:
これは、あなたが実行したい具体的な操作を定義する部分です。
例えば、addSpeakerという名前のMutationを作るとき、
これが実際に「話者を追加する」という操作を行う定義になります。

入力(Input):
Mutationがデータを変更するために必要な情報を提供するためのものです。
addSpeakerの場合、話者の名前や経歴など、話者を追加するために必要な情報が入力として必要になります。
慣例として、この入力の型はAddSpeakerInputのように、Mutationの名前にInputを付けた形で命名されます。

ペイロード(Payload):
これは、Mutationの実行後に返されるデータの型です。
操作の結果として何が起こったのか、または新しく追加された話者の詳細など、
クライアントが受け取りたい情報を含みます。
慣例に従い、この型はAddSpeakerPayloadのように、Mutationの名前にPayloadを付けた形で命名されます。
```

`AddSpeakerInput.cs`を追加
```AddSpeakerInput.cs
namespace ConferencePlanner.GraphQL
{
    public record AddSpeakerInput(
        string Name,
        string Bio,
        string WebSite);
}
```

`AddSpeakerPayload.cs`を追加
```AddSpeakerPayload.cs
using ConferencePlanner.GraphQL.Data;

namespace ConferencePlanner.GraphQL
{
    public class AddSpeakerPayload
    {
        public AddSpeakerPayload(Speaker speaker)
        {
            Speaker = speaker;
        }

        public Speaker Speaker { get; }
    }
}
```

`mutation type`を定義した`Mutation.cs`を追加
```cs
using System.Threading.Tasks;
using ConferencePlanner.GraphQL.Data;
using HotChocolate;

namespace ConferencePlanner.GraphQL
{
    public class Mutation
    {
        public async Task<AddSpeakerPayload> AddSpeakerAsync(
            AddSpeakerInput input,
            [Service] ApplicationDbContext context)
        {
            var speaker = new Speaker
            {
                Name = input.Name,
                Bio = input.Bio,
                WebSite = input.WebSite
            };

            context.Speakers.Add(speaker);
            await context.SaveChangesAsync();

            return new AddSpeakerPayload(speaker);
        }
    }
}

```

`Program.cs`に`Mutation`を追加
```Program.cs
builder.Services
	.AddGraphQLServer()
    .AddQueryType<Query>()
	.AddMutationType<Mutation>(); // 追加
```

サーバーを起動
```bash
dotnet run --project GraphQL
```

サーバーが起動した後、`http://localhost:5042/graphql`の`schema Definition`から追加した`Mutaion`が確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/fa40f207-bcfc-95fa-6cd9-8fa7b0dd0c69.png)

`Operation`に以下の`mutation`を記述して`Run`をクリックすると、
```graphql
mutation AddSpeaker {
  addSpeaker(input: {
    name: "Speaker Name"
    bio: "Speaker Bio"
    webSite: "http://speaker.website" }) {
    speaker {
      id
    }
  }
}
```

レスポンスが返却される
```json
{
  "data": {
    "addSpeaker": {
      "speaker": {
        "id": 1
      }
    }
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/718fae28-fb0f-91a0-26dd-61f37b523612.png)

`Operation`に`GetSpeakerNames`のクエリを記述して`Run`をクリックすると、
```graphql
query GetSpeakerNames {
  speakers {
    name
  }
}
```

`mutation`によって実行された結果を取得できる
```json
{
  "data": {
    "speakers": [
      {
        "name": "Speaker Name"
      }
    ]
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/e2dda522-d9b2-b65b-e5fd-e9c747f85960.png)

実行時の`GraphQL`サーバーのログ
`INSERT`と`SELECT`のクエリが走っていることが確認できた
```bash
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (11ms) [Parameters=[@p0='?' (Size = 11), @p1='?' (Size = 12), @p2='?' (Size = 22)], CommandType='Text', CommandTimeout='30']    
      INSERT INTO "Speakers" ("Bio", "Name", "WebSite")
      VALUES (@p0, @p1, @p2)
      RETURNING "Id";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "s"."Id", "s"."Bio", "s"."Name", "s"."WebSite"
      FROM "Speakers" AS "s"
```

ソースコードは[GitHub](https://github.com/pixcelo/CSharp-app/tree/main/ConferencePlanner)に置きました

## 2. null許容・非許容のコントロール

- GraphQLの型はデフォルトでnull許容
- C#の型はデフォルトでnull許容ではない

nullを許容する場合、`GraphQL.csproj`で`<Nullable>enable</Nullable>`とする
```GraphQL.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="HotChocolate.AspNetCore" Version="13.9.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>
```
`Entity Framework`が実行時にSpeakersプロパティを初期化するため、コンパイラが不必要に警告を発するのを避けるために、null許容性に関するデフォルトの振る舞いをオーバーライドする (default==nullだが、!をつけてnullにならないという意)
```ApplicationDBContext.cs
 public DbSet<Speaker> Speakers { get; set; } = default!;
```

`Speaker.cs`クラスのプロパティにて、プロパティを設定する
`[Required]`アノテーションを付与するとNULL非許容となる
C#の場合、`string`(null非許容参照型)と `string?`(null許容参照型)を`?`の有無で設定する (※C#8.0以降)
```Speaker.cs
using System.ComponentModel.DataAnnotations;

namespace ConferencePlanner.GraphQL.Data
{
    public class Speaker
    {
        public int Id { get; set; }

        [Required]
        [StringLength(200)]
        public string? Name { get; set; }

        [StringLength(4000)]
        public string? Bio { get; set; }

        [StringLength(1000)]
        public virtual string? WebSite { get; set; }
    }
}
```

```AddSpeakerInput.cs
namespace ConferencePlanner.GraphQL
{
    public record AddSpeakerInput(
        string Name,
        string? Bio,
        string? WebSite);
}
```
`GraphQL`スキーマで確認すると、`!`サフィックスが付与されているものがnull非許容で定義されていることが確認できる
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/80a1e6ff-7295-1feb-76e8-0a8b76ce4391.png">

- `string` null許容
- `string!` null非許容
- `[Speaker!]!` オブジェクトの配列がnull非許容かつ、配列の要素すべてがnull非許容

詳しくは公式ドキュメント [Object types and fields](https://graphql.org/learn/schema/#object-types-and-fields) を参照

続く

## Reference
- [GraphQL](https://graphql.org/)
- [GraphQL と HotChocolate の概要](https://learn.microsoft.com/ja-jp/shows/on-net/getting-started-with-hotchocolate)
- [Getting started with GraphQL in .NET - Michael Staib - NDC Copenhagen 2022](https://www.youtube.com/watch?v=qrh97hToWpM)
- [ChilliCream/graphql-platform](https://github.com/ChilliCream/graphql-platform)
- [ChilliCream/graphql-workshop](https://github.com/ChilliCream/graphql-workshop)
- [GraphQL Highway](https://speakerdeck.com/cockscomb/graphql-highway)
- [relay.dev](https://relay.dev/)
- [.NET 8.0 Runtime (v8.0.2) - Windows x64](https://dotnet.microsoft.com/ja-jp/download/dotnet/thank-you/runtime-8.0.2-windows-x64-installer)