GraphQL Server で GraphQL.EntityFramework を使う

ASP.NET Core 用の GraphQL Server を使うことで、 ASP.NET Core プロジェクトに GraphQL API を実装するのが楽になりそうだった。

tnakamura.hatenablog.com

ただ、ASP.NET Core MVC でエンドポイントを自作する必要はなくなるけど、resolver は依然書く必要がある。 ミューテーションはともかくクエリなら、GraphQL.EntityFramework を使うことで、さらに実装を楽にできるかも。

tnakamura.hatenablog.com

というわけで試してみた。

using GraphQL;
using GraphQL.EntityFramework;
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
{
    //============================================
    // 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();
            }
        }
    }

    //==============================
    // 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);
        }
    }

    // クエリを定義
    public class HelloGraphQLQuery : EfObjectGraphType
    {
        public HelloGraphQLQuery(IEfGraphQLService graphQLService)
            : base(graphQLService)
        {
            AddQueryConnectionField<PlayerType, Player>(
                name: "players",
                resolve: context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    return dbContext.Players.AsQueryable();
                });
        }
    }

    // ミューテーションを定義
    // ミューテーションでは GraphQL.EntityFramework のサポートは無いので、
    // GraphQL.NET で実装したときのまま。
    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>();
        }
    }

    //==============================================================
    // 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 =>
                {
                    // UserContext として ApplicationDbContext を
                    // resolver に渡す。
                    return httpContext.RequestServices
                        .GetService<ApplicationDbContext>();
                });

            //============================
            // GraphQL.EntityFramework 用
            //============================

            // コネクション用の型(ConnectionType<T>, EdgeType<T>, PageInfoType)
            // を DI コンテナに登録する
            EfGraphQLConventions.RegisterConnectionTypesInContainer(services);

            // GraphQL.EntityFramework が使う型(EfGraphQLServer とか)を登録する。
            // DbContext は EfGraphQLServer に渡されるが、
            // EfGraphQLServer が必要なのは DbContext.Model だけなので、
            // DbContext のインスタンスは即 Dispose してしまっていい。
            using (var dbContext = services.BuildServiceProvider().GetService<ApplicationDbContext>())
            {
                EfGraphQLConventions.RegisterInContainer(services, dbContext);
            }

            // 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>();
    }
}

GraphQL Playground でクエリを実行してみる。 フィルタ用の where やソート用の order といった引数を GraphQL.EntityFramework が生成してくれている。

f:id:griefworker:20181226161329p:plain

外部に公開する API なら導入をちょっと躊躇するけど、 内部で使う API ならこれでいいかもしれないな。 個人開発なので、できればサクッと実装を済ませたいし。