diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs index 79a32b8..ed973e3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs @@ -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; @@ -54,7 +55,10 @@ public static IServiceCollection ConfigureApplication(this IServiceCollection se opts.ContainerName = optionsValue.BlobStorage.ContainerName; }) .AddScoped(); - #endif +#endif + + services.AddMemoryCache(); + services.AddSingleton(); return services; } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryById.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryById.cs index 9abb5b7..19a5978 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryById.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryById.cs @@ -9,7 +9,10 @@ namespace Monaco.Template.Backend.Application.Features.Country; public sealed class GetCountryById { - public record Query(Guid Id) : QueryByIdBase(Id); + public record Query(Guid Id) : CachedQueryByIdBase(Id) + { + public override string CacheKey => $"get-country-by-id-{Id}"; + } public sealed class Handler : IRequestHandler { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryList.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryList.cs index 77fc7d5..8f21e12 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryList.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/GetCountryList.cs @@ -10,7 +10,10 @@ namespace Monaco.Template.Backend.Application.Features.Country; public sealed class GetCountryList { - public record Query(IEnumerable> QueryString) : QueryBase>(QueryString); + public record Query(IEnumerable> QueryString) : CachedQueryBase>(QueryString) + { + public override string CacheKey => $"get-country-list-{GetQueryStringHashCode()}"; + } public sealed class Handler : IRequestHandler> { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/CachedQueryService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/CachedQueryService.cs new file mode 100644 index 0000000..3fcc20b --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/CachedQueryService.cs @@ -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 GetOrCreateAsync(string cacheKey, + Func> factory, + TimeSpan? expiration = null, + CancellationToken cancellationToken = default) + => await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.SetAbsoluteExpiration(expiration ?? DefaultExpiration); + return factory(cancellationToken); + }); +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Behaviors/QueryCachingBehavior.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Behaviors/QueryCachingBehavior.cs new file mode 100644 index 0000000..b2c61be --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Behaviors/QueryCachingBehavior.cs @@ -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 : IPipelineBehavior where TRequest : ICachedQuery +{ + private readonly ICachedQueryService _cachedQueryService; + + public QueryCachingBehavior(ICachedQueryService cachedQueryService) + { + _cachedQueryService = cachedQueryService; + } + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + => _cachedQueryService.GetOrCreateAsync(request.CacheKey, + _ => next(), + request.Expiration, + cancellationToken)!; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryBase.cs new file mode 100644 index 0000000..c0156d9 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryBase.cs @@ -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(IEnumerable> QueryString) : QueryBase(QueryString), ICachedQuery +{ + public abstract string CacheKey { get; } + public virtual TimeSpan? Expiration => null; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByIdBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByIdBase.cs new file mode 100644 index 0000000..6e87a0d --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByIdBase.cs @@ -0,0 +1,9 @@ +using Monaco.Template.Backend.Common.Application.Queries.Contracts; + +namespace Monaco.Template.Backend.Common.Application.Queries; + +public abstract record CachedQueryByIdBase(Guid Id) : QueryByIdBase(Id), ICachedQuery +{ + public abstract string CacheKey { get; } + public virtual TimeSpan? Expiration => null; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByKeyBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByKeyBase.cs new file mode 100644 index 0000000..0ce0a3e --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryByKeyBase.cs @@ -0,0 +1,9 @@ +using Monaco.Template.Backend.Common.Application.Queries.Contracts; + +namespace Monaco.Template.Backend.Common.Application.Queries; + +public abstract record CachedQueryByKeyBase(TKey Key) : QueryByKeyBase(Key), ICachedQuery +{ + public abstract string CacheKey { get; } + public virtual TimeSpan? Expiration => null; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryPagedBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryPagedBase.cs new file mode 100644 index 0000000..7d0a41f --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/CachedQueryPagedBase.cs @@ -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(IEnumerable> QueryString) : QueryPagedBase(QueryString), ICachedQuery +{ + public abstract string CacheKey { get; } + public virtual TimeSpan? Expiration => null; +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQuery.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQuery.cs new file mode 100644 index 0000000..0498c49 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQuery.cs @@ -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 : IRequest, ICachedQuery; \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQueryService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQueryService.cs new file mode 100644 index 0000000..2c80ae0 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Contracts/ICachedQueryService.cs @@ -0,0 +1,9 @@ +namespace Monaco.Template.Backend.Common.Application.Queries.Contracts; + +public interface ICachedQueryService +{ + Task GetOrCreateAsync(string key, + Func> factory, + TimeSpan? expiration = null, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs index 68b4a13..9675577 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs @@ -59,4 +59,15 @@ protected bool Expand(string value) => QueryString.Any(x => x.Key.Equals("expand .Value .Select(x => Enum.TryParse(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; + } } \ No newline at end of file