GraphQL.NET でのページネーション

GraphQL でコレクションデータの取得やページネーションを実現する場合、 Relay 由来の Connection や Edge といったインタフェースを定義するのが定石になっているみたい。

GraphQL.NET はそれらをサポートしていて、ConnectionType や EdgeType といった型を提供してくれていたので、試しにやってみた。

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

namespace ConnectionSample
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Number { 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.Number).IsRequired();
                e.HasKey(x => x.Id);
            });
        }

        // テスト用のデータを登録する
        public void EnsureSeedData()
        {
            if (!Players.Any())
            {
                foreach (var x in Enumerable.Range(1, 200))
                {
                    Players.Add(new Player
                    {
                        Name = $"プレーヤー{x}",
                        Number = x,
                    });
                }
                SaveChanges();
            }
        }
    }

    public class PlayerType : ObjectGraphType<Player>
    {
        public PlayerType()
            : base()
        {
            Field(x => x.Id);
            Field(x => x.Name);
            Field(x => x.Number);
        }
    }

    public class SampleQuery : ObjectGraphType
    {
        public SampleQuery()
            : base()
        {
            Connection<PlayerType>()
                .Name("players")
                .PageSize(50)
                .Resolve(context =>
                {
                    var dbContext = (ApplicationDbContext)context.UserContext;
                    var query = dbContext.Players.AsQueryable();

                    if (!string.IsNullOrEmpty(context.After) &&
                        int.TryParse(context.After, out var lastPlayerId))
                    {
                        query = query.Where(x => x.Id > lastPlayerId);
                    }

                    if (context.First != null)
                    {
                        query = query.Take(context.First.Value);
                    }
                    else
                    {
                        query = query.Take(context.PageSize.Value);
                    }

                    var players = query.ToList();

                    return new Connection<Player>
                    {
                        PageInfo = new PageInfo
                        {
                            StartCursor = players.FirstOrDefault()?.Id.ToString(),
                            EndCursor = players.LastOrDefault()?.Id.ToString(),
                        },
                        Edges = players.Select(x => new Edge<Player>
                        {
                            Cursor = x.Id.ToString(),
                            Node = x,
                        }).ToList(),
                    };
                });
        }
    }

    public class SampleSchema : Schema
    {
        public SampleSchema(IDependencyResolver dependencyResolver)
            : base(dependencyResolver)
        {
            Query = dependencyResolver.Resolve<SampleQuery>();
        }
    }

    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>();
                })
                .AddRelayGraphTypes(); // ConnectionType<T>, EdgeType<T>, PageInfoType 等を登録

            // GraphQL 用
            services.AddSingleton<PlayerType>();
            services.AddSingleton<SampleQuery>();
            services.AddSingleton<ISchema, SampleSchema>();
            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 API を起動して、Web ブラウザから Playground にアクセスし、コレクションデータを取得するクエリを実行してみたのがこちら。

f:id:griefworker:20190117105455p:plain

ちゃんと動いた。次のページのデータを取得する場合は、endCursor の値を引数 after に指定したクエリを実行すればいい。

GraphQL.EntityFramework を使ったときは、リゾルバで IQueryable<T> を返してやれば良きに計らってくれたのに対し、GraphQL.NET だけだと自前で書かないといけないのは手間だな。