diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 15fc0f613..af85c9ab0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using GraphQL.Utilities; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Infrastructure; @@ -22,14 +23,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private readonly IMemoryCache cache; private readonly IServiceProvider resolver; + private readonly GraphQLOptions options; - public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver) + public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver, IOptions options) { Guard.NotNull(cache, nameof(cache)); Guard.NotNull(resolver, nameof(resolver)); + Guard.NotNull(options, nameof(options)); this.cache = cache; this.resolver = resolver; + this.options = options.Value; } public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) @@ -81,21 +85,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private Task GetModelAsync(IAppEntity app) { + if (options.CacheDuration <= 0) + { + return CreateModelAsync(app); + } + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); return cache.GetOrCreateAsync(cacheKey, async entry => { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var allSchemas = await resolver.GetRequiredService().GetSchemasAsync(app.Id); - - return new GraphQLModel(app, - allSchemas, - resolver.GetRequiredService(), - resolver.GetRequiredService()); + return await CreateModelAsync(app); }); } + private async Task CreateModelAsync(IAppEntity app) + { + var allSchemas = await resolver.GetRequiredService().GetSchemasAsync(app.Id); + + return new GraphQLModel(app, + allSchemas, + resolver.GetRequiredService(), + resolver.GetRequiredService()); + } + private static object CreateCacheKey(DomainId appId, string etag) { return $"GraphQLModel_{appId}_{etag}"; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs new file mode 100644 index 000000000..de436341c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class GraphQLOptions + { + public int CacheDuration { get; set; } = 10 * 60; + } +} diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs index 5c27169b5..ec89bbd8a 100644 --- a/backend/src/Squidex/Config/Domain/QueryServices.cs +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -21,6 +21,8 @@ namespace Squidex.Config.Domain { var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); + services.Configure(config, "graphql"); + services.AddSingletonAs(c => ActivatorUtilities.CreateInstance(c, exposeSourceUrl)) .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 742c88ca1..339f35f82 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using FakeItEasy; using GraphQL.DataLoader; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Squidex.Domain.Apps.Core; @@ -139,52 +140,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return serializer.Serialize(result); } - public sealed class TestServiceProvider : IServiceProvider - { - private readonly Dictionary services; - - public TestServiceProvider(GraphQLTestBase testBase) - { - var appProvider = A.Fake(); - - A.CallTo(() => appProvider.GetSchemasAsync(testBase.appId.Id)) - .Returns(new List - { - testBase.schema, - testBase.schemaRef1, - testBase.schemaRef2, - testBase.schemaInvalidName - }); - - var dataLoaderContext = new DataLoaderContextAccessor(); - - var urlGenerator = new FakeUrlGenerator(); - - services = new Dictionary - { - [typeof(IAppProvider)] = appProvider, - [typeof(IAssetQueryService)] = testBase.assetQuery, - [typeof(ICommandBus)] = testBase.commandBus, - [typeof(IContentQueryService)] = testBase.contentQuery, - [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, - [typeof(IUrlGenerator)] = urlGenerator, - [typeof(ISemanticLog)] = A.Fake(), - [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext), - [typeof(SharedTypes)] = new SharedTypes(urlGenerator) - }; - } - - public object GetService(Type serviceType) - { - return services.GetOrDefault(serviceType); - } - } - private CachingGraphQLService CreateSut() { var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - return new CachingGraphQLService(cache, new TestServiceProvider(this)); + var appProvider = A.Fake(); + + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + .Returns(new List + { + schema, + schemaRef1, + schemaRef2, + schemaInvalidName + }); + + var dataLoaderContext = (IDataLoaderContextAccessor)new DataLoaderContextAccessor(); + var dataLoaderListener = new DataLoaderDocumentListener(dataLoaderContext); + + var services = + new ServiceCollection() + .AddMemoryCache() + .AddSingleton(A.Fake()) + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .AddSingleton(commandBus) + .AddSingleton(contentQuery) + .AddSingleton(dataLoaderContext) + .AddSingleton(dataLoaderListener) + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + + return new CachingGraphQLService(cache, services, Options.Create(new GraphQLOptions())); } } } diff --git a/backend/tests/docker-compose.yml b/backend/tests/docker-compose.yml index 75c4fff5e..3b4d513d9 100644 --- a/backend/tests/docker-compose.yml +++ b/backend/tests/docker-compose.yml @@ -18,6 +18,7 @@ services: - IDENTITY__ADMINCLIENTSECRET=xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0= - STORE__MONGODB__CONFIGURATION=mongodb://mongo - STORE__TYPE=MongoDB + - GRAPHQL__CACHEDURATION=0 networks: - internal depends_on: diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index 701f6383c..89cb03ee8 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -19,7 +19,6 @@ using Xunit; namespace TestSuite.ApiTests { - [Trait("Category", "NotAutomated")] public sealed class GraphQLTests : IClassFixture { public ContentFixture _ { get; }