Browse Source

Timeouts and token cleanup.

pull/728/head
Sebastian 5 years ago
parent
commit
913ef7066d
  1. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs
  2. 116
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs
  4. 48
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  5. 3
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  6. 82
      backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs
  7. 24
      backend/src/Squidex/appsettings.json
  8. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  9. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

6
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetOptions public sealed class AssetOptions
@ -14,5 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
public int MaxResults { get; set; } = 200; public int MaxResults { get; set; } = 200;
public long MaxSize { get; set; } = 5 * 1024 * 1024; public long MaxSize { get; set; } = 5 * 1024 * 1024;
public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5);
} }
} }

116
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Log; using Squidex.Log;
@ -22,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAssetLoader assetLoader; private readonly IAssetLoader assetLoader;
private readonly IAssetFolderRepository assetFolderRepository; private readonly IAssetFolderRepository assetFolderRepository;
private readonly AssetOptions options;
private readonly AssetQueryParser queryParser; private readonly AssetQueryParser queryParser;
public AssetQueryService( public AssetQueryService(
@ -29,18 +31,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
IAssetRepository assetRepository, IAssetRepository assetRepository,
IAssetLoader assetLoader, IAssetLoader assetLoader,
IAssetFolderRepository assetFolderRepository, IAssetFolderRepository assetFolderRepository,
IOptions<AssetOptions> options,
AssetQueryParser queryParser) AssetQueryParser queryParser)
{ {
Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(assetLoader, nameof(assetLoader)); Guard.NotNull(assetLoader, nameof(assetLoader));
Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository)); Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository));
Guard.NotNull(options, nameof(options));
Guard.NotNull(queryParser, nameof(queryParser)); Guard.NotNull(queryParser, nameof(queryParser));
this.assetEnricher = assetEnricher; this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.assetLoader = assetLoader; this.assetLoader = assetLoader;
this.assetFolderRepository = assetFolderRepository; this.assetFolderRepository = assetFolderRepository;
this.options = options.Value;
this.queryParser = queryParser; this.queryParser = queryParser;
} }
@ -53,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
while (id != DomainId.Empty) while (id != DomainId.Empty)
{ {
var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id, ct); var folder = await FindFolderCoreAsync(appId, id, ct);
if (folder == null || result.Any(x => x.Id == folder.Id)) if (folder == null || result.Any(x => x.Id == folder.Id))
{ {
@ -75,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
using (Profiler.TraceMethod<AssetQueryService>()) using (Profiler.TraceMethod<AssetQueryService>())
{ {
var assetFolders = await assetFolderRepository.QueryAsync(appId, parentId, ct); var assetFolders = await QueryFoldersCoreAsync(appId, parentId, ct);
return assetFolders; return assetFolders;
} }
@ -86,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
using (Profiler.TraceMethod<AssetQueryService>()) using (Profiler.TraceMethod<AssetQueryService>())
{ {
var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId, ct); var assetFolders = await QueryFoldersCoreAsync(context, parentId, ct);
return assetFolders; return assetFolders;
} }
@ -99,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
using (Profiler.TraceMethod<AssetQueryService>()) using (Profiler.TraceMethod<AssetQueryService>())
{ {
var asset = await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, ct); var asset = await FindByHashCoreAsync(context, hash, fileName, fileSize, ct);
if (asset == null) if (asset == null)
{ {
@ -117,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
using (Profiler.TraceMethod<AssetQueryService>()) using (Profiler.TraceMethod<AssetQueryService>())
{ {
var asset = await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, ct); var asset = await FindBySlugCoreAsync(context, slug, ct);
if (asset == null) if (asset == null)
{ {
@ -135,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
using (Profiler.TraceMethod<AssetQueryService>()) using (Profiler.TraceMethod<AssetQueryService>())
{ {
var asset = await assetRepository.FindAssetAsync(id, ct); var asset = await FindCoreAsync(id, ct);
if (asset == null) if (asset == null)
{ {
@ -161,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
} }
else else
{ {
asset = await assetRepository.FindAssetAsync(context.App.Id, id, ct); asset = await FindCoreAsync(context, id, ct);
} }
if (asset == null) if (asset == null)
@ -187,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
q = await queryParser.ParseAsync(context, q); q = await queryParser.ParseAsync(context, q);
var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q, ct); var assets = await QueryCoreAsync(context, parentId, q, ct);
if (q.Ids != null && q.Ids.Count > 0) if (q.Ids != null && q.Ids.Count > 0)
{ {
@ -222,5 +227,100 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return await assetEnricher.EnrichAsync(assets, context, ct); return await assetEnricher.EnrichAsync(assets, context, ct);
} }
} }
private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(Context context, DomainId parentId,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token);
}
}
}
private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(DomainId appId, DomainId parentId,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token);
}
}
}
private async Task<IResultList<IAssetEntity>> QueryCoreAsync(Context context, DomainId? parentId, Q q,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token);
}
}
}
private async Task<IAssetFolderEntity?> FindFolderCoreAsync(DomainId appId, DomainId id, CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token);
}
}
}
private async Task<IAssetEntity?> FindByHashCoreAsync(Context context, string hash, string fileName, long fileSize,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token);
}
}
}
private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token);
}
}
}
private async Task<IAssetEntity?> FindCoreAsync(DomainId id,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetAsync(id, combined.Token);
}
}
}
private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token);
}
}
}
} }
} }

6
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentOptions public sealed class ContentOptions
@ -12,5 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
public int DefaultPageSize { get; set; } = 200; public int DefaultPageSize { get; set; } = 200;
public int MaxResults { get; set; } = 200; public int MaxResults { get; set; } = 200;
public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5);
} }
} }

48
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -29,25 +30,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
private readonly IContentLoader contentLoader; private readonly IContentLoader contentLoader;
private readonly ContentQueryParser queryParser; private readonly ContentQueryParser queryParser;
private readonly ContentOptions options;
public ContentQueryService( public ContentQueryService(
IAppProvider appProvider, IAppProvider appProvider,
IContentEnricher contentEnricher, IContentEnricher contentEnricher,
IContentRepository contentRepository, IContentRepository contentRepository,
IContentLoader contentLoader, IContentLoader contentLoader,
IOptions<ContentOptions> options,
ContentQueryParser queryParser) ContentQueryParser queryParser)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(contentEnricher, nameof(contentEnricher)); Guard.NotNull(contentEnricher, nameof(contentEnricher));
Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentLoader, nameof(contentLoader)); Guard.NotNull(contentLoader, nameof(contentLoader));
Guard.NotNull(options, nameof(options));
Guard.NotNull(queryParser, nameof(queryParser)); Guard.NotNull(queryParser, nameof(queryParser));
this.appProvider = appProvider; this.appProvider = appProvider;
this.contentEnricher = contentEnricher; this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
this.contentLoader = contentLoader; this.contentLoader = contentLoader;
this.queryParser = queryParser; this.options = options.Value;
this.queryParser = queryParser; this.queryParser = queryParser;
} }
@ -68,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
else else
{ {
content = await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), ct); content = await FindCoreAsync(context, id, schema, ct);
} }
if (content == null || content.SchemaId.Id != schema.Id) if (content == null || content.SchemaId.Id != schema.Id)
@ -101,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
q = await queryParser.ParseAsync(context, q, schema); q = await queryParser.ParseAsync(context, q, schema);
var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), ct); var contents = await QueryCoreAsync(context, q, schema, ct);
if (q.Ids != null && q.Ids.Count > 0) if (q.Ids != null && q.Ids.Count > 0)
{ {
@ -133,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
q = await queryParser.ParseAsync(context, q); q = await queryParser.ParseAsync(context, q);
var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), ct); var contents = await QueryCoreAsync(context, q, schemas, ct);
if (q.Ids != null && q.Ids.Count > 0) if (q.Ids != null && q.Ids.Count > 0)
{ {
@ -217,6 +221,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList(); return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList();
} }
private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, ISchemaEntity schema,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), ct);
}
}
}
private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, List<ISchemaEntity> schemas,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), ct);
}
}
}
private async Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token);
}
}
}
private static bool IsAccessible(ISchemaEntity schema) private static bool IsAccessible(ISchemaEntity schema)
{ {
return schema.SchemaDef.IsPublished; return schema.SchemaDef.IsPublished;

3
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -55,6 +55,9 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddSingletonAs<ApiPermissionUnifier>() services.AddSingletonAs<ApiPermissionUnifier>()
.As<IClaimsTransformation>(); .As<IClaimsTransformation>();
services.AddSingletonAs<TokenStoreInitializer>()
.AsSelf();
services.AddSingletonAs<CreateAdminInitializer>() services.AddSingletonAs<CreateAdminInitializer>()
.AsSelf(); .AsSelf();

82
backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs

@ -0,0 +1,82 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using OpenIddict.Abstractions;
using OpenIddict.MongoDb;
using OpenIddict.MongoDb.Models;
using Squidex.Hosting;
using Squidex.Infrastructure.Timers;
namespace Squidex.Areas.IdentityServer.Config
{
public sealed class TokenStoreInitializer : IInitializable
{
private readonly OpenIddictMongoDbOptions options;
private readonly IServiceProvider serviceProvider;
private CompletionTimer timer;
public TokenStoreInitializer(IOptions<OpenIddictMongoDbOptions> options,
IServiceProvider serviceProvider)
{
this.options = options.Value;
this.serviceProvider = serviceProvider;
}
public async Task InitializeAsync(CancellationToken ct)
{
await SetupIndexAsync(ct);
await PruneAsync(ct);
timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, async ct =>
{
await PruneAsync(ct);
});
}
private async Task PruneAsync(CancellationToken ct)
{
using (var scope = serviceProvider.CreateScope())
{
var tokenManager = scope.ServiceProvider.GetRequiredService<IOpenIddictTokenManager>();
await tokenManager.PruneAsync(DateTimeOffset.UtcNow.AddDays(-40), ct);
}
}
private async Task SetupIndexAsync(CancellationToken ct)
{
using (var scope = serviceProvider.CreateScope())
{
var database = await scope.ServiceProvider.GetRequiredService<IOpenIddictMongoDbContext>().GetDatabaseAsync(ct);
var collection = database.GetCollection<OpenIddictMongoDbToken<string>>(options.TokensCollectionName);
await collection.Indexes.CreateOneAsync(
new CreateIndexModel<OpenIddictMongoDbToken<string>>(
Builders<OpenIddictMongoDbToken<string>>.IndexKeys
.Ascending(x => x.ReferenceId)),
cancellationToken: ct);
}
}
public async Task ReleaseAsync(CancellationToken ct)
{
if (timer != null)
{
await timer.StopAsync();
}
}
}
}

24
backend/src/Squidex/appsettings.json

@ -274,7 +274,17 @@
* *
* Warning: Use pagination and not large number of items. * Warning: Use pagination and not large number of items.
*/ */
"maxResults": 200 "maxResults": 200,
/*
* The timeout when searching for single items in the database.
*/
"timeoutFind": "00:00:01",
/*
* The timeout when searching for multiple items in the database.
*/
"timeoutQuery": "00:00:05"
}, },
"assets": { "assets": {
@ -303,7 +313,17 @@
/* /*
* True to delete assets files permanently. * True to delete assets files permanently.
*/ */
"deletePermanent": false "deletePermanent": false,
/*
* The timeout when searching for single items in the database.
*/
"timeoutFind": "00:00:01",
/*
* The timeout when searching for multiple items in the database.
*/
"timeoutQuery": "00:00:05"
}, },
"logging": { "logging": {

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -38,7 +39,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._)) A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
sut = new AssetQueryService(assetEnricher, assetRepository, assetLoader, assetFolderRepository, queryParser); var options = Options.Create(new AssetOptions());
sut = new AssetQueryService(
assetEnricher,
assetRepository,
assetLoader,
assetFolderRepository,
options,
queryParser);
} }
[Fact] [Fact]

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -11,6 +11,7 @@ using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
@ -58,11 +59,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<ISchemaEntity?>._)) A.CallTo(() => queryParser.ParseAsync(A<Context>._, A<Q>._, A<ISchemaEntity?>._))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!)); .ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
var options = Options.Create(new ContentOptions());
sut = new ContentQueryService( sut = new ContentQueryService(
appProvider, appProvider,
contentEnricher, contentEnricher,
contentRepository, contentRepository,
contentVersionLoader, contentVersionLoader,
options,
queryParser); queryParser);
} }

Loading…
Cancel
Save