mirror of https://github.com/Squidex/squidex.git
Browse Source
* Graphql memory cache. * Mini improvement. * Fix registrations. * Simplification. * Just some reformatting.pull/852/head
committed by
GitHub
52 changed files with 760 additions and 217 deletions
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public sealed class AssetCache : QueryCache<DomainId, IEnrichedAssetEntity>, IAssetCache |
||||
|
{ |
||||
|
public AssetCache(IMemoryCache? memoryCache, IOptions<AssetOptions> options) |
||||
|
: base(options.Value.CanCache ? memoryCache : null) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public interface IAssetCache : IQueryCache<DomainId, IEnrichedAssetEntity> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ContentCache : QueryCache<DomainId, IEnrichedContentEntity>, IContentCache |
||||
|
{ |
||||
|
public ContentCache(IMemoryCache? memoryCache, IOptions<ContentOptions> options) |
||||
|
: base(options.Value.CanCache ? memoryCache : null) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using GraphQL.Types; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives |
||||
|
{ |
||||
|
public sealed class CacheDirective : DirectiveGraphType |
||||
|
{ |
||||
|
public CacheDirective() |
||||
|
: base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment) |
||||
|
{ |
||||
|
Description = "Enable Memory Caching"; |
||||
|
|
||||
|
Arguments = new QueryArguments(new QueryArgument<IntGraphType> |
||||
|
{ |
||||
|
Name = "duration", |
||||
|
Description = "Cache duration in seconds.", |
||||
|
DefaultValue = 600 |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IContentCache : IQueryCache<DomainId, IEnrichedContentEntity> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public interface IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> |
||||
|
{ |
||||
|
void SetMany(IEnumerable<(TKey, T?)> results, |
||||
|
TimeSpan? permanentDuration = null); |
||||
|
|
||||
|
Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, |
||||
|
TimeSpan? permanentDuration = null); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Concurrent; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public class QueryCache<TKey, T> : IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> |
||||
|
{ |
||||
|
private readonly ConcurrentDictionary<TKey, T?> entries = new ConcurrentDictionary<TKey, T?>(); |
||||
|
private readonly IMemoryCache? memoryCache; |
||||
|
|
||||
|
public QueryCache(IMemoryCache? memoryCache = null) |
||||
|
{ |
||||
|
this.memoryCache = memoryCache; |
||||
|
} |
||||
|
|
||||
|
public void SetMany(IEnumerable<(TKey, T?)> results, |
||||
|
TimeSpan? permanentDuration = null) |
||||
|
{ |
||||
|
Guard.NotNull(results); |
||||
|
|
||||
|
foreach (var (key, value) in results) |
||||
|
{ |
||||
|
Set(key, value, permanentDuration); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void Set(TKey key, T? value, |
||||
|
TimeSpan? permanentDuration = null) |
||||
|
{ |
||||
|
entries[key] = value; |
||||
|
|
||||
|
if (memoryCache != null && permanentDuration > TimeSpan.Zero) |
||||
|
{ |
||||
|
memoryCache.Set(key, value, permanentDuration.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, |
||||
|
TimeSpan? permanentDuration = null) |
||||
|
{ |
||||
|
Guard.NotNull(keys); |
||||
|
Guard.NotNull(query); |
||||
|
|
||||
|
var items = GetMany(keys, permanentDuration.HasValue); |
||||
|
|
||||
|
var pendingIds = new HashSet<TKey>(keys.Where(key => !items.ContainsKey(key))); |
||||
|
|
||||
|
if (pendingIds.Count > 0) |
||||
|
{ |
||||
|
var queried = (await query(pendingIds)).ToDictionary(x => x.Id); |
||||
|
|
||||
|
foreach (var id in pendingIds) |
||||
|
{ |
||||
|
queried.TryGetValue(id, out var item); |
||||
|
|
||||
|
items[id] = item; |
||||
|
|
||||
|
Set(id, item, permanentDuration); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return items.Values.NotNull().ToList(); |
||||
|
} |
||||
|
|
||||
|
private Dictionary<TKey, T?> GetMany(IEnumerable<TKey> keys, |
||||
|
bool fromPermanentCache = false) |
||||
|
{ |
||||
|
var result = new Dictionary<TKey, T?>(); |
||||
|
|
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
if (entries.TryGetValue(key, out var value)) |
||||
|
{ |
||||
|
result[key] = value; |
||||
|
} |
||||
|
else if (fromPermanentCache && memoryCache != null && memoryCache.TryGetValue(key, out value)) |
||||
|
{ |
||||
|
result[key] = value; |
||||
|
|
||||
|
entries[key] = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure |
||||
|
{ |
||||
|
public interface IWithId<T> |
||||
|
{ |
||||
|
T Id { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,180 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public class QueryCacheTests |
||||
|
{ |
||||
|
private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
|
||||
|
private record CachedEntry(int Value) : IWithId<int> |
||||
|
{ |
||||
|
public int Id => Value; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_from_cache() |
||||
|
{ |
||||
|
var sut = new QueryCache<int, CachedEntry>(); |
||||
|
|
||||
|
var (queried, result) = await ConfigureAsync(sut, 1, 2); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, queried); |
||||
|
Assert.Equal(new[] { 1, 2 }, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_pending_from_cache() |
||||
|
{ |
||||
|
var sut = new QueryCache<int, CachedEntry>(); |
||||
|
|
||||
|
var (queried1, result1) = await ConfigureAsync(sut, 1, 2); |
||||
|
var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
||||
|
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_pending_from_cache_if_manually_added() |
||||
|
{ |
||||
|
var sut = new QueryCache<int, CachedEntry>(); |
||||
|
|
||||
|
sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); |
||||
|
|
||||
|
var (queried, result) = await ConfigureAsync(sut, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 3, 4 }, queried); |
||||
|
Assert.Equal(new[] { 2, 3, 4 }, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_pending_from_memory_cache_if_manually_added() |
||||
|
{ |
||||
|
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
|
||||
|
var cacheDuration = TimeSpan.FromSeconds(10); |
||||
|
|
||||
|
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); |
||||
|
|
||||
|
var (queried, result) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 3, 4 }, queried); |
||||
|
Assert.Equal(new[] { 2, 3, 4 }, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_added_permanently() |
||||
|
{ |
||||
|
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
|
||||
|
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); |
||||
|
|
||||
|
var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, queried); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_queried_permanently() |
||||
|
{ |
||||
|
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
|
||||
|
var cacheDuration = TimeSpan.FromSeconds(10); |
||||
|
|
||||
|
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); |
||||
|
|
||||
|
var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, queried); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_query_again_if_failed_before() |
||||
|
{ |
||||
|
var sut = new QueryCache<int, CachedEntry>(); |
||||
|
|
||||
|
var (queried1, result1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); |
||||
|
var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
||||
|
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
||||
|
|
||||
|
Assert.Equal(new[] { 2 }, result1.ToArray()); |
||||
|
Assert.Equal(new[] { 2, 3, 4 }, result2.ToArray()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_query_from_memory_cache() |
||||
|
{ |
||||
|
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
|
||||
|
var cacheDuration = TimeSpan.FromSeconds(10); |
||||
|
|
||||
|
var (queried1, result1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); |
||||
|
var (queried2, result2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
||||
|
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_query_from_memory_cache_if_not_queried_permanently() |
||||
|
{ |
||||
|
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
||||
|
|
||||
|
var (queried1, result1) = await ConfigureAsync(sut1, x => true, null, 1, 2); |
||||
|
var (queried2, result2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); |
||||
|
|
||||
|
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
||||
|
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
||||
|
} |
||||
|
|
||||
|
private static Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, params int[] ids) |
||||
|
{ |
||||
|
return ConfigureAsync(sut, x => true, null, ids); |
||||
|
} |
||||
|
|
||||
|
private static async Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, Func<int, bool> predicate, TimeSpan? cacheDuration, params int[] ids) |
||||
|
{ |
||||
|
var queried = new HashSet<int>(); |
||||
|
|
||||
|
var result = await sut.CacheOrQueryAsync(ids, async pending => |
||||
|
{ |
||||
|
queried.AddRange(pending); |
||||
|
|
||||
|
await Task.Yield(); |
||||
|
|
||||
|
return pending.Where(predicate).Select(x => new CachedEntry(x)); |
||||
|
}, cacheDuration); |
||||
|
|
||||
|
return (queried.ToArray(), result.Select(x => x.Value).ToArray()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue