ASP.NET Core で GraphQL API の Mutation を実装する

以前、ASP.NET Core で GraphQL API のサンプルを実装してみた。

tnakamura.hatenablog.com

当時必要だったのはクエリだけだったので、ミューテーションは後回しにしていたけど、 とうとうミューテーションが必要になってしまった。 真面目に GraphQL API を実装するなら、ミューテーションの実装は避けられない。

まずはお試しということで、簡単なクエリとミューテーションを持つ GraphQL API を実装してみた。 ASP.NET Core MVC と Entity Framework Core を使っている。

using GraphiQl;
using GraphQL;
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 System.Linq;
using System.Threading.Tasks;

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 MVC で GraphQL エンドポイントを実装
    //====================================================

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

        public string Query { 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<IActionResult> Post([FromBody] GraphQLModel model)
        {
            var result = await documentExecuter.ExecuteAsync(new ExecutionOptions
            {
                Schema = schema,
                OperationName = model.OperationName,
                Query = model.Query,
                UserContext = dbContext,
            });
            return Ok(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_2);

            // 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)
        {
            app.UseGraphiQl("/graphiql", "/graphql");

            app.UseMvc();

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

デバッグ実行して動作確認。 まずは登録した初期データをクエリで取得してみる。

f:id:griefworker:20181213171800p:plain

選手を 1 人追加してみる。 ストーブリーグで注目の日本人選手筆頭、堂安を追加。

f:id:griefworker:20181213171829p:plain

ちゃんと追加されたかクエリで確認。

f:id:griefworker:20181213171840p:plain

追加されていたので、今度は選手の情報を更新してみる。 メッシは右サイドが主戦場なので、RWG に変更。

f:id:griefworker:20181213171915p:plain

更新されたかクエリで確認。

f:id:griefworker:20181213171929p:plain

ポジションを RWG に変更できていた。 最後に選手の情報を削除してみる。 ベイルはよく負傷で離脱するので登録削除。

f:id:griefworker:20181213171938p:plain

削除されたことをクエリで確認。

f:id:griefworker:20181213171946p:plain

削除成功。

単純な CRUD だけど、GraphQL API で実現できた。 クエリとミューテーション、どちらも実装方法は似たようなもの。 今回のサンプル程度でわかる明確な違いは、クエリは resolve 時にデータベースからデータを取得して返すのに対し、 ミューテーションは resolve 時にデータベースに変更を加える、という点くらい。