ASP.NET Core と Entity Framework Core を使った GraphQL API の実装が楽になるかもしれないライブラリ GraphQL.EntityFramework

先日 ASP.NET Core で GraphQL API を実装してみた。

tnakamura.hatenablog.com

データベースから Entity Framework Core を使ってデータを取得する部分は自前で書いたわけだが、なかなか面倒だったのでなんとかしたいところ。

例えば Ruby だと graphql-ruby という gem がメジャーで、こいつは Rails をサポートしているので、 ActiveRecord と連携しやすそうだ。Entity Framework Core でも同様のものがほしい。

探せばあるもので、GraphQL.EntityFramework というパッケージがあった。

www.nuget.org

これを使えば ASP.NET Core + Entity Framework Core で GraphQL を実装するのが少し楽になりそう。先日のサンプルを書き変えてみた。

using GraphiQl;
using GraphQL;
using GraphQL.EntityFramework;
using GraphQL.Types;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

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

    public class Team
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public IList<Player> Players { get; set; }
    }

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

        public Team Team { get; set; }
    }

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

        public DbSet<Team> Teams => Set<Team>();

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

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

        // テスト用のデータを登録する
        public void EnsureSeedData()
        {
            if (!Teams.Any())
            {
                Teams.Add(new Team
                {
                    Name = "バルセロナ",
                    Players = new List<Player>
                    {
                        new Player
                        {
                            Name = "メッシ",
                            Number = 10,
                            Position = "FW",
                        },
                        new Player
                        {
                            Name = "スアレス",
                            Number = 9,
                            Position = "FW",
                        },
                    },
                });
                Teams.Add(new Team
                {
                    Name = "レアルマドリード",
                    Players = new List<Player>
                    {
                        new Player
                        {
                            Name = "ベイル",
                            Number = 11,
                            Position = "FW",
                        },
                        new Player
                        {
                            Name = "モドリッチ",
                            Number = 10,
                            Position = "MF",
                        },
                    },
                });

                SaveChanges();
            }
        }
    }

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

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

    // Team に対応する GraphQL の型
    public class TeamType : EfObjectGraphType<Team>
    {
        public TeamType(IEfGraphQLService graphQLService)
            : base(graphQLService)
        {
            Field(x => x.Id);
            Field(x => x.Name);
            Field<ListGraphType<PlayerType>>(
                name: "players",
                resolve: context => context.Source.Players);
        }
    }

    // クエリを定義
    public class HelloGraphQLQuery : EfObjectGraphType
    {
        public HelloGraphQLQuery(IEfGraphQLService graphQLService)
            : base(graphQLService)
        {
            AddQueryConnectionField<TeamType, Team>(
                name: "teams",
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    return dbContext.Teams.Include(t => t.Players);
                });
        }
    }

    // GraphQL のスキーマを定義
    public class HelloGraphQLSchema : Schema
    {
        public HelloGraphQLSchema(IDependencyResolver dependencyResolver)
            : base(dependencyResolver)
        {
            // 今回はクエリだけ
            Query = dependencyResolver.Resolve<HelloGraphQLQuery>();
        }
    }

    //====================================================
    // 3. ASP.NET Core MVC で GraphQL エンドポイントを実装
    //====================================================

    // クライアントから送信されてきた GraphQL をバインドするモデル
    public class GraphQLModel
    {
        public string OperationName { get; set; }

        public string Query { get; set; }

        public JObject Variables { get; set; }
    }

    [Route("graphql")]
    [ApiController]
    public class GraphQLController : ControllerBase
    {
        readonly IDocumentExecuter documentExecuter;

        readonly ISchema schema;

        readonly ApplicationDbContext dbContext;

        public GraphQLController(
            IDocumentExecuter documentExecuter,
            ISchema schema,
            ApplicationDbContext dbContext)
        {
            this.documentExecuter = documentExecuter;
            this.schema = schema;
            this.dbContext = dbContext;
        }

        [HttpPost]
        public async Task<ExecutionResult> Post([FromBody] GraphQLModel model)
        {
            var result = await documentExecuter.ExecuteAsync(new ExecutionOptions
            {
                Schema = schema,
                OperationName = model.OperationName,
                Query = model.Query,
                Inputs = model.Variables?.ToInputs(),
                UserContext = dbContext,
            });

            if (0 < result.Errors?.Count)
            {
                Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }

            return result;
        }
    }

    //==============================================================
    // 4. いつものように 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")));

            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            //--------------------
            // ここから GraphQL 用
            //--------------------

            EfGraphQLConventions.RegisterConnectionTypesInContainer(services);
            using (var dbContext = services.BuildServiceProvider().GetService<ApplicationDbContext>())
            {
                EfGraphQLConventions.RegisterInContainer(services, dbContext);
            }

            // GraphQL 用のクラスを DI コンテナに登録しておく必要がある。
            services.AddSingleton<HelloGraphQLQuery>();
            services.AddSingleton<TeamType>();
            services.AddSingleton<PlayerType>();
            services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
            services.AddSingleton<ISchema, HelloGraphQLSchema>();
            services.AddSingleton<IDependencyResolver>(
                provider => new FuncDependencyResolver(provider.GetService));
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ApplicationDbContext context)
        {
            // GraphiQL をホストして手軽に GraphQL を試せるようにする
            app.UseGraphiQl("/graphiql", "/graphql");

            app.UseMvc();

            // テスト用データベースが無ければ作る
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
            context.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>();
    }
}

デバッグ実行し Web ブラウザで /graqhiql にアクセス。 全チーム取得するクエリを実行してみると、無事データベースからデータを引っ張ってこれた。

f:id:griefworker:20181115153328p:plain

GraphQL.EntityFramework がフィルタやソートのためのパラメータを自動で生成してくれるから、自分で実装する手間が省けるのは助かる。ただ、それよりも追加で覚えることが多いのと、API が好みでないので、実践投入するかと聞かれたら微妙なところ。