From c1bc9e7edaf22131217e4adbb7c265dcc0075ef9 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 21 Jun 2019 22:01:55 +0200 Subject: [PATCH] DataLoader implemented. --- .../Contents/GraphQL/CachingGraphQLService.cs | 46 ++++------- .../GraphQL/GraphQLExecutionContext.cs | 78 ++++++++++++++++--- .../Contents/GraphQL/GraphQLModel.cs | 18 ++--- .../Contents/ScheduleJob.cs | 3 +- src/Squidex/Config/Domain/EntitiesServices.cs | 14 ++++ .../Contents/GraphQL/GraphQLTestBase.cs | 41 +++++++--- 6 files changed, 137 insertions(+), 63 deletions(-) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index e56e28cb3..1a01b229a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -8,43 +8,25 @@ using System; using System.Linq; using System.Threading.Tasks; +using GraphQL; using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IContentQueryService contentQuery; - private readonly IGraphQLUrlGenerator urlGenerator; - private readonly ISemanticLog log; - private readonly IAssetQueryService assetQuery; - private readonly IAppProvider appProvider; - - public CachingGraphQLService( - IMemoryCache cache, - IAppProvider appProvider, - IAssetQueryService assetQuery, - IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator, - ISemanticLog log) + private readonly IDependencyResolver resolver; + + public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) : base(cache) { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(urlGenerator, nameof(urlGenerator)); - Guard.NotNull(log, nameof(log)); - - this.appProvider = appProvider; - this.assetQuery = assetQuery; - this.contentQuery = contentQuery; - this.urlGenerator = urlGenerator; - this.log = log; + Guard.NotNull(resolver, nameof(resolver)); + + this.resolver = resolver; } public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries) @@ -54,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator); + var ctx = new GraphQLExecutionContext(context, resolver); var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); @@ -68,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator); + var ctx = new GraphQLExecutionContext(context, resolver); var result = await QueryInternalAsync(model, ctx, query); @@ -82,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return (false, new { data = new object() }); } - var result = await model.ExecuteAsync(ctx, query, log); + var result = await model.ExecuteAsync(ctx, query); if (result.Errors?.Any() == true) { @@ -102,9 +84,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var allSchemas = await appProvider.GetSchemasAsync(app.Id); + var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); - return new GraphQLModel(app, allSchemas, contentQuery.DefaultPageSizeGraphQl, assetQuery.DefaultPageSizeGraphQl, urlGenerator); + return new GraphQLModel(app, + allSchemas, + resolver.Resolve().DefaultPageSizeGraphQl, + resolver.Resolve().DefaultPageSizeGraphQl, + resolver.Resolve()); }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 54a2793ab..c2587b747 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -7,37 +7,93 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using GraphQL; +using GraphQL.DataLoader; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public sealed class GraphQLExecutionContext : QueryExecutionContext { + private static readonly List EmptyAssets = new List(); + private static readonly List EmptyContents = new List(); + private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; + private readonly IDependencyResolver resolver; + public IGraphQLUrlGenerator UrlGenerator { get; } - public GraphQLExecutionContext(QueryContext context, - IAssetQueryService assetQuery, - IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator) - : base(context, assetQuery, contentQuery) + public ISemanticLog Log { get; } + + public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver) + : base(context, + resolver.Resolve(), + resolver.Resolve()) + { + UrlGenerator = resolver.Resolve(); + + dataLoaderContextAccessor = resolver.Resolve(); + + this.resolver = resolver; + } + + public void Setup(ExecutionOptions execution) { - UrlGenerator = urlGenerator; + var loader = resolver.Resolve(); + + execution.Listeners.Add(loader); + execution.FieldMiddleware.Use(LoggingMiddleware.Create(resolver.Resolve())); + + execution.UserContext = this; } - public Task> GetReferencedAssetsAsync(IJsonValue value) + public async Task> GetReferencedAssetsAsync(IJsonValue value) { var ids = ParseIds(value); - return GetReferencedAssetsAsync(ids); + if (ids == null) + { + return EmptyAssets; + } + + var dataLoader = + dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", + async batch => + { + var result = await GetReferencedAssetsAsync(new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + + var assets = await Task.WhenAll(ids.Select(x => dataLoader.LoadAsync(x))); + + return assets.Where(x => x != null).ToList(); } - public Task> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) + public async Task> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) { var ids = ParseIds(value); - return GetReferencedContentsAsync(schemaId, ids); + if (ids == null) + { + return EmptyContents; + } + + var dataLoader = + dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"Schema_{schemaId}", + async batch => + { + var result = await GetReferencedContentsAsync(schemaId, new List(batch)); + + return result.ToDictionary(x => x.Id); + }); + + var contents = await Task.WhenAll(ids.Select(x => dataLoader.LoadAsync(x))); + + return contents.Where(x => x != null).ToList(); } private static ICollection ParseIds(IJsonValue value) @@ -58,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } catch { - return new List(); + return null; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index 9cb3dc67e..5f4ee3be3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; using GraphQLSchema = GraphQL.Types.Schema; #pragma warning disable IDE0003 @@ -172,20 +171,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return contentTypes.GetOrAdd(schema, s => new ContentGraphType()); } - public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query, ISemanticLog log) + public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) { Guard.NotNull(context, nameof(context)); - var inputs = query.Variables?.ToInputs(); - - var result = await new DocumentExecuter().ExecuteAsync(options => + var result = await new DocumentExecuter().ExecuteAsync(execution => { - options.FieldMiddleware.Use(LoggingMiddleware.Create(log)); - options.OperationName = query.OperationName; - options.UserContext = context; - options.Schema = graphQLSchema; - options.Inputs = inputs; - options.Query = query.Query; + context.Setup(execution); + + execution.Schema = graphQLSchema; + execution.Inputs = query.Variables?.ToInputs(); + execution.Query = query.Query; }).ConfigureAwait(false); return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs index 4145dd954..fe48b89e4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs @@ -1,5 +1,4 @@ - -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 1b378c173..99092c745 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using GraphQL; +using GraphQL.DataLoader; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -74,6 +76,18 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); + services.AddSingletonAs(x => new FuncDependencyResolver(t => x.GetRequiredService(t))) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 53e155449..7012ae1fd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Security.Claims; using FakeItEasy; +using GraphQL; +using GraphQL.DataLoader; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -34,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLTestBase { - protected readonly Schema schemaDef; protected readonly Guid schemaId = Guid.NewGuid(); protected readonly Guid appId = Guid.NewGuid(); protected readonly string appName = "my-app"; @@ -42,8 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL protected readonly IAssetQueryService assetQuery = A.Fake(); protected readonly ISchemaEntity schema = A.Fake(); protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); - protected readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - protected readonly IAppProvider appProvider = A.Fake(); + protected readonly IDependencyResolver dependencyResolver; protected readonly IAppEntity app = A.Dummy(); protected readonly QueryContext context; protected readonly ClaimsPrincipal user = new ClaimsPrincipal(); @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public GraphQLTestBase() { - schemaDef = + var schemaDef = new Schema("my-schema") .AddJson(1, "my-json", Partitioning.Invariant, new JsonFieldProperties()) @@ -93,11 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => schema.Id).Returns(schemaId); A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - var allSchemas = new List { schema }; - - A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas); - - sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator(), A.Fake()); + sut = CreateSut(); } protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) @@ -210,5 +206,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { return serializer.Serialize(result); } + + private CachingGraphQLService CreateSut() + { + var appProvider = A.Fake(); + + A.CallTo(() => appProvider.GetSchemasAsync(appId)) + .Returns(new List { schema }); + + var dataLoaderContext = new DataLoaderContextAccessor(); + + var services = new Dictionary + { + [typeof(IAppProvider)] = appProvider, + [typeof(IAssetQueryService)] = assetQuery, + [typeof(IContentQueryService)] = contentQuery, + [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, + [typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(), + [typeof(ISemanticLog)] = A.Fake(), + [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) + }; + + var resolver = new FuncDependencyResolver(t => services[t]); + + var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + + return new CachingGraphQLService(cache, resolver); + } } }