🛸

.NET6 + EnityFramework + ChocolateでGraphQLサーバーを作る

2022/09/29に公開

業務では、いつもはREST APIでバックエンドは作成していたのですが、
.NETでGraphQLサーバーを作ってみようかと思い作ったので記録のために記事にしました。

仕様技術

  • .NET6
  • Entity Framework
  • GraphQL
  • JWT Authentication

GraphQL用ライブラリ

https://graphql.org/code/#c-net
いくつか候補がありますが、見た感じ結構面白そうだったので以下のライブラリを使用しました。

アプリ作成

基本的にはドキュメント通りに進めていけば大丈夫。
https://chillicream.com/docs/hotchocolate/get-started

まずはVS2022でASP.NET Core(空)プロジェクトを作成します。

必要パッケージをNuGet経由でインストール

今回はEntityFrameworkも使用するので諸々インストールしました。
DB(Oracle)やEF関係の説明は記事主旨と脱線してしまうので割愛します。

  • HotChocolate.AspNetCore(本体)
  • HotChocolate.AspNetCore.Authorization(認可)
  • HotChocolate.Data.EntityFramework(EnitytyFramework用)
  • Microsoft.AspNetCore.Authentication.JwtBearer(JWT認証)
  • Microsoft.AspNetCore.Authorization(JWT認証)
  • Microsoft.EntityFrameworkCore
  • Microsoft.ENtityFrameworkCore.Design(スキャフォールディング用)
  • Microsoft.ENtityFrameworkCore.Tools(スキャフォールディング用)
  • Oracle.EntityFrameworkCore(Oracle)
  • Oracle.ManagedDataAccess.Core(Oracle)

サービス追加とエンドポイント作成

Program.cs
// namespace ResolversディレクトリにQuery Mutationクラスを作成
using WebApplication1.Resolvers; 

var builder = WebApplication.CreateBuilder(args);

// JWT認証認可
var singningKey = new SymmetricSecurityKey(
        System.Text.Encoding.UTF8.GetBytes(builder.Configuration["JWT:IssuerSigningKey"])
    );

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateLifetime = true,
                            ValidateIssuerSigningKey = true,
                            ValidIssuer = builder.Configuration["JWT:ValidIssuer"],
                            ValidAudience = builder.Configuration["JWT:ValidAudience"],
                            IssuerSigningKey = singningKey
                        };
                });
builder.Services.AddAuthorization();
// JWT発行用サービス
builder.Services.AddScoped<AuthService>();

// GraphQLサービス追加 今回はQueryとMutationを追加
builder.Services.AddGraphQLServer()
                .RegisterDbContext<MyDbContext>(DbContextKind.Resolver) // MyDbContextを作成済とする
                .AddQueryType<Query>() // Query
                .AddMutationType<Mutation>()
                .AddType<UploadType>(); // FileUpload
		
		
var app = builder.Build();
app.UseRouting();
// エンドポイント
app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", (ctx) =>
    {
        ctx.Response.Redirect("/graphql");
        return Task.FromResult(Task.CompletedTask);
    });

    // GraphQL Root
    // {root}/graphql
    endpoints.MapGraphQL();
});

app.Run();

Query、Mutation作成

  • Query
Resolvers/Query.cs
// Microsoft.AspNetCore.AuthorizationAttributeは使用しない
using HotChocolate.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Resolvers
{
    public class Query
    {
        private readonly AuthService _authService;

        public Query(AuthService authService) 
	{
	}

        public Author GetAuthor() =>
        new Author
        {
            Name = "Noripi10"
        };
	
	[Authorize]
	public Author GetAuthorSecret() =>
        new Author
        {
            Name = "Noripi10(Secret)"
        };
    }
}
  • Mutation
Resolvers/Mutation.cs
using HotChocolate.AspNetCore.Authorization;
using WebApplication1.Models;
using WebApplication1.Services;

namespace WebApplication1.Resolvers
{
    public class Mutation
    {
        private readonly AuthService _authService;

        public Mutation(AuthService authService)
        {
            _authService = authService;
        }

	public AuthTokenResult Login([Service] MyDbContext _db, string userId)
        {
            var user = _db.Users.Where(e => userId.Equals(e.UserId)).ToList().FirstOrDefault();
            if (user != null)
            {
		// ここでJWTを発行して返却
                return _authService.GenerateToken(userId);
            }

            throw new UnauthorizedAccessException();
        }
    }
}
  • ディレクトリ構成はこのような感じです。

  • トークン作成(AuthService)は以下

        public AuthTokenResult GenerateToken(string user)
        {
            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user)
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:IssuerSigningKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: _config["JWT:ValidIssuer"],
                audience: _config["JWT:ValidAudience"],
                claims: claims,
                // 有効期限
                expires: DateTime.Now.AddDays(1),
                signingCredentials: creds
            );

            var resutlToken = new JwtSecurityTokenHandler().WriteToken(token);

            return new AuthTokenResult { token = resutlToken };
        }

デバッグ(F5)

無事起動すると以下のような画面が起動します
BananaCakePop(バナナケーキホップ) 名前がオシャレ!

Create Documentを押すと実行画面が立ち上がるので実行してみます。
すると無事Query結果が返却されました。

次にログイン(Mutation)処理を実行してみます。
すると無事Token(JWT)が返却されました。このTokenを使用して認可処理を行います。

Query処理に「GetAuthorSecret」をというログインしていないと動作しない処理(※[Authorize]を付与している)を作成していたので、こちら実行していきます。そのまま実行すると認証エラーにが返ってきます。

先ほど取得したTokenwoヘッダーにセットして実行してみます。実行結果が返ってきました。
ちゃんと動いています。

フロントエンド(クライアント側)

フロントエンドは今回ReactNative+ApoloClientで実装しています。

@apollo/client
apollo-upload-client

    const authLink = createUploadLink({
      uri: baseUrl,
      fetch,
      headers: {
        // トークンセット
        authorization: token ? `Bearer ${token}` : '',
      },
    });
    
    const client = new ApolloClient({
      link: authLink,
      cache,
    });

.NETクライアント用にライブラリも用意されてます
Strawberry Shake(ストロベリーシェイク) 名前がオシャレ!

終わり

.NETでGraphQLサーバーを立ててみましたがライブラリのおかげで簡単に実装できました。
本番環境では、ファイル送信、Cronosを使ったジョブサービス、メール通知サービスなどを実装しています。何よりもEntityFrameworkが安心だし便利すぎます!!

コードファーストに切り替え中です。

Discussion