Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Add query caching pipeline behavior #75

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#if (filesSupport)
using Monaco.Template.Backend.Common.BlobStorage.Extensions;
#endif
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Application.DependencyInjection;

Expand Down Expand Up @@ -54,7 +55,10 @@ public static IServiceCollection ConfigureApplication(this IServiceCollection se
opts.ContainerName = optionsValue.BlobStorage.ContainerName;
})
.AddScoped<IFileService, FileService>();
#endif
#endif

services.AddMemoryCache();
services.AddSingleton<ICachedQueryService, CachedQueryService>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ namespace Monaco.Template.Backend.Application.Features.Country;

public sealed class GetCountryById
{
public record Query(Guid Id) : QueryByIdBase<CountryDto?>(Id);
public record Query(Guid Id) : CachedQueryByIdBase<CountryDto?>(Id)
{
public override string CacheKey => $"get-country-by-id-{Id}";
}

public sealed class Handler : IRequestHandler<Query, CountryDto?>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ namespace Monaco.Template.Backend.Application.Features.Country;

public sealed class GetCountryList
{
public record Query(IEnumerable<KeyValuePair<string, StringValues>> QueryString) : QueryBase<List<CountryDto>>(QueryString);
public record Query(IEnumerable<KeyValuePair<string, StringValues>> QueryString) : CachedQueryBase<List<CountryDto>>(QueryString)
{
public override string CacheKey => $"get-country-list-{GetQueryStringHashCode()}";
}

public sealed class Handler : IRequestHandler<Query, List<CountryDto>>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Caching.Memory;
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Application.Services;

public class CachedQueryService : ICachedQueryService
{
private static readonly TimeSpan DefaultExpiration = TimeSpan.FromMinutes(5);
private readonly IMemoryCache _memoryCache;

public CachedQueryService(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}

public async Task<T?> GetOrCreateAsync<T>(string cacheKey,
Func<CancellationToken, Task<T>> factory,
TimeSpan? expiration = null,
CancellationToken cancellationToken = default)
=> await _memoryCache.GetOrCreateAsync(cacheKey, entry =>
{
entry.SetAbsoluteExpiration(expiration ?? DefaultExpiration);
return factory(cancellationToken);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using MediatR;
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Common.Application.Queries.Behaviors;

public sealed class QueryCachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : ICachedQuery
{
private readonly ICachedQueryService _cachedQueryService;

public QueryCachingBehavior(ICachedQueryService cachedQueryService)
{
_cachedQueryService = cachedQueryService;
}

public Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
=> _cachedQueryService.GetOrCreateAsync(request.CacheKey,
_ => next(),
request.Expiration,
cancellationToken)!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Extensions.Primitives;
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Common.Application.Queries;

public abstract record CachedQueryBase<T>(IEnumerable<KeyValuePair<string, StringValues>> QueryString) : QueryBase<T>(QueryString), ICachedQuery<T>
{
public abstract string CacheKey { get; }
public virtual TimeSpan? Expiration => null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Common.Application.Queries;

public abstract record CachedQueryByIdBase<T>(Guid Id) : QueryByIdBase<T>(Id), ICachedQuery<T>
{
public abstract string CacheKey { get; }
public virtual TimeSpan? Expiration => null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Common.Application.Queries;

public abstract record CachedQueryByKeyBase<T, TKey>(TKey Key) : QueryByKeyBase<T, TKey>(Key), ICachedQuery<T>
{
public abstract string CacheKey { get; }
public virtual TimeSpan? Expiration => null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Extensions.Primitives;
using Monaco.Template.Backend.Common.Application.Queries.Contracts;

namespace Monaco.Template.Backend.Common.Application.Queries;

public abstract record CachedQueryPagedBase<T>(IEnumerable<KeyValuePair<string, StringValues>> QueryString) : QueryPagedBase<T>(QueryString), ICachedQuery<T>
{
public abstract string CacheKey { get; }
public virtual TimeSpan? Expiration => null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using MediatR;

namespace Monaco.Template.Backend.Common.Application.Queries.Contracts;

public interface ICachedQuery
{
public abstract string CacheKey { get; }
public TimeSpan? Expiration { get; }
}

public interface ICachedQuery<T> : IRequest<T>, ICachedQuery;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Monaco.Template.Backend.Common.Application.Queries.Contracts;

public interface ICachedQueryService
{
Task<T?> GetOrCreateAsync<T>(string key,
Func<CancellationToken, Task<T>> factory,
TimeSpan? expiration = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,15 @@ protected bool Expand(string value) => QueryString.Any(x => x.Key.Equals("expand
.Value
.Select(x => Enum.TryParse<TEnum>(x, true, out var y) ? y : (TEnum?)null)
.FirstOrDefault(x => x is not null);

public int GetQueryStringHashCode()
{
int hash = 17;
foreach (var (key, value) in QueryString)
{
hash = (hash * 23) + key.GetHashCode();
hash = (hash * 23) + value.GetHashCode();
}
return hash;
}
}