ASP.NET Core 用 GraphQL Server 実装

ASP.NET Core MVC を使って GraphQL API のエンドポイントを実装してきたけど、ASP.NET Core 用の GraphQL Server 実装が存在することを今更知った。

www.nuget.org

気分は某CMの松重豊。「それさぁ、早くいってよぉ~」って感じだ。 いやまぁ、ちゃんと調べなかった自分が悪いんだけど。

Web ブラウザ上で GraphQL を試せる GraphQL IDE も、 GraphiQL だけでなく GraphQL Playground とかに対応しているみたいだ。

www.nuget.org

先日のサンプルGraphQL.Server.Transports.AspNetCore を使ったものに書き換えてみた。GraphQL IDE は GraphiQL ではなく GraphQL Playground を選択。

using GraphQL;
using GraphQL.Server;
using GraphQL.Server.Ui.Playground;
using GraphQL.Types;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;

namespace HelloGraphQL
{
    //============================================
    // 1. Entity Framework Core で使うクラスを定義
    //============================================

    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Position { get; set; }
    }

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Player> Players => Set<Player>();

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Player>(e =>
            {
                e.Property(x => x.Id).IsRequired();
                e.Property(x => x.Name).IsRequired();
                e.Property(x => x.Position).IsRequired();
                e.HasKey(x => x.Id);
            });
        }

        // テスト用のデータを登録する
        public void EnsureSeedData()
        {
            if (!Players.Any())
            {
                Players.AddRange(
                    new Player
                    {
                        Name = "メッシ",
                        Position = "FW",
                    },
                    new Player
                    {
                        Name = "スアレス",
                        Position = "FW",
                    },
                    new Player
                    {
                        Name = "ベイル",
                        Position = "FW",
                    },
                    new Player
                    {
                        Name = "モドリッチ",
                        Position = "MF",
                    }
                );
                SaveChanges();
            }
        }
    }

    //==============================
    // 2. GraphQL で使うクラスを定義
    //==============================

    // Player に対応する GraphQL の型
    public class PlayerType : ObjectGraphType<Player>
    {
        public PlayerType()
        {
            Field(x => x.Id);
            Field(x => x.Name);
            Field(x => x.Position);
        }
    }

    // クエリを定義
    public class HelloGraphQLQuery : ObjectGraphType
    {
        public HelloGraphQLQuery()
        {
            Field<ListGraphType<PlayerType>>(
                "players",
                arguments: new QueryArguments(
                    new QueryArgument<IntGraphType>
                    {
                        Name = "id",
                    }),
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    var query = dbContext.Players.AsQueryable();
                    if (context.HasArgument("id"))
                    {
                        var id = context.GetArgument<int>("id");
                        query = query.Where(b => b.Id == id);
                    }
                    return query.ToList();
                });
        }
    }

    // ミューテーションを定義
    public class HelloGraphQLMutation : ObjectGraphType
    {
        public HelloGraphQLMutation()
        {
            Field<PlayerType>(
                "createPlayer",
                arguments: new QueryArguments(
                    new QueryArgument<NonNullGraphType<StringGraphType>>()
                    {
                        Name = "name",
                    },
                    new QueryArgument<NonNullGraphType<StringGraphType>>()
                    {
                        Name = "position",
                    }
                ),
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    var name = context.GetArgument<string>("name");
                    var position = context.GetArgument<string>("position");
                    var player = new Player()
                    {
                        Name = name,
                        Position = position,
                    };
                    dbContext.Players.Add(player);
                    dbContext.SaveChanges();
                    return player;
                });

            Field<PlayerType>(
                "updatePlayer",
                arguments: new QueryArguments(
                    new QueryArgument<NonNullGraphType<IntGraphType>>()
                    {
                        Name = "id",
                    },
                    new QueryArgument<StringGraphType>()
                    {
                        Name = "name",
                    },
                    new QueryArgument<StringGraphType>()
                    {
                        Name = "position",
                    }
                ),
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    var id = context.GetArgument<int>("id");
                    var player = dbContext.Players.Find(id);
                    if (player != null)
                    {
                        if (context.HasArgument("name"))
                        {
                            player.Name = context.GetArgument<string>("name");
                        }
                        if (context.HasArgument("position"))
                        {
                            player.Position = context.GetArgument<string>("position");
                        }
                        dbContext.Players.Update(player);
                        dbContext.SaveChanges();
                        return player;
                    }
                    else
                    {
                        return null;
                    }
                });

            Field<PlayerType>(
                "deletePlayer",
                arguments: new QueryArguments(
                    new QueryArgument<NonNullGraphType<IntGraphType>>()
                    {
                        Name = "id",
                    }
                ),
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    var id = context.GetArgument<int>("id");
                    var player = dbContext.Players.Find(id);
                    if (player != null)
                    {
                        dbContext.Players.Remove(player);
                        dbContext.SaveChanges();
                        return player;
                    }
                    else
                    {
                        return null;
                    }
                });
        }
    }

    // GraphQL のスキーマを定義
    public class HelloGraphQLSchema : Schema
    {
        public HelloGraphQLSchema(IDependencyResolver dependencyResolver)
            : base(dependencyResolver)
        {
            Query = dependencyResolver.Resolve<HelloGraphQLQuery>();
            Mutation = dependencyResolver.Resolve<HelloGraphQLMutation>();
        }
    }

    //==============================================================
    // 3. ASP.NET Core 用の Startup と Program を作成
    //==============================================================

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            // GraphQL Server 用
            services.AddGraphQL()
                .AddUserContextBuilder(httpContext =>
                {
                    return httpContext.RequestServices
                        .GetService<ApplicationDbContext>();
                });

            // GraphQL 用
            services.AddSingleton<PlayerType>();
            services.AddSingleton<HelloGraphQLQuery>();
            services.AddSingleton<HelloGraphQLMutation>();
            services.AddSingleton<ISchema, HelloGraphQLSchema>();
            services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
            services.AddSingleton<IDependencyResolver>(x => new FuncDependencyResolver(x.GetService));
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext dbContext)
        {
            // GraphQL Server を使う
            app.UseGraphQL<ISchema>("/graphql");

            // GraphQL Playground を使う
            app.UseGraphQLPlayground(new GraphQLPlaygroundOptions
            {
                Path = "/ui/playground",
            });

            // テスト用データベースが無ければ作る
            dbContext.Database.EnsureCreated();
            dbContext.EnsureSeedData();
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

デバッグ実行して、/ui/playground を表示。 クエリを実行してみたところ、ちゃんと GraphQL API として動作した。

f:id:griefworker:20181214142158p:plain

AddUserContextBuilder のところで単純に ApplicationDbContext を返したけど、独自のコンテキストを定義して、その中にいろいろ必要な値を詰めてやれば、この GraphQL Server でも結構やれそう。まず気になるのは認証・認可なので、次はそこら辺を調べたい。