diff --git a/NClient.sln b/NClient.sln index 2db60cbf8..e9124e905 100644 --- a/NClient.sln +++ b/NClient.sln @@ -125,6 +125,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NClient.DotNetTool.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NClient.Providers.Serialization.SystemTextJson.Tests", "tests\NClient.Providers\NClient.Providers.Serialization.SystemTextJson.Tests\NClient.Providers.Serialization.SystemTextJson.Tests.csproj", "{FEFE8BA5-1FFF-43F2-A09F-C0F3194413E9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NClient.Providers.Caching.Redis", "src\NClient.Providers\NClient.Providers.Caching.Redis\NClient.Providers.Caching.Redis.csproj", "{3A811C1A-63C2-4E2D-BC17-2029047729B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NClient.Providers.Caching.Redis.Tests", "tests\NClient.Providers\NClient.Providers.Caching.Redis.Tests\NClient.Providers.Caching.Redis.Tests.csproj", "{04DFFDB6-46E3-42B4-96A7-0F98F2833126}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -368,6 +372,18 @@ Global {FEFE8BA5-1FFF-43F2-A09F-C0F3194413E9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FEFE8BA5-1FFF-43F2-A09F-C0F3194413E9}.Release|Any CPU.Build.0 = Release|Any CPU {FEFE8BA5-1FFF-43F2-A09F-C0F3194413E9}.TestRelease|Any CPU.ActiveCfg = Release|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.Release|Any CPU.Build.0 = Release|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.TestRelease|Any CPU.ActiveCfg = Debug|Any CPU + {3A811C1A-63C2-4E2D-BC17-2029047729B1}.TestRelease|Any CPU.Build.0 = Debug|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.Release|Any CPU.Build.0 = Release|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.TestRelease|Any CPU.ActiveCfg = Debug|Any CPU + {04DFFDB6-46E3-42B4-96A7-0F98F2833126}.TestRelease|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -420,6 +436,8 @@ Global {7BA62D11-E7B6-4879-903E-AC718C79DA96} = {F3AF19D7-1D80-4449-98CC-96BC13FF8103} {4EA0CDB4-8C81-4F10-BDC9-4DF8AE638D25} = {7BA62D11-E7B6-4879-903E-AC718C79DA96} {FEFE8BA5-1FFF-43F2-A09F-C0F3194413E9} = {B1BCB5AE-6A5E-49F7-A00A-6B2931DF628C} + {3A811C1A-63C2-4E2D-BC17-2029047729B1} = {DCB997FD-29F4-48D9-8348-093761DA42E7} + {04DFFDB6-46E3-42B4-96A7-0F98F2833126} = {B1BCB5AE-6A5E-49F7-A00A-6B2931DF628C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {244A4C58-8FA7-496C-ABCF-72F333149A20} diff --git a/Tests.slnf b/Tests.slnf index 66e84a952..33aa0ed2d 100644 --- a/Tests.slnf +++ b/Tests.slnf @@ -8,6 +8,7 @@ "tests\\NClient.Providers\\NClient.Providers.Api.Rest.Tests\\NClient.Providers.Api.Rest.Tests.csproj", "tests\\NClient.Providers\\NClient.Providers.Mapping.HttpResponses.Tests\\NClient.Providers.Mapping.HttpResponses.Tests.csproj", "tests\\NClient.Providers\\NClient.Providers.Mapping.LanguageExt.Tests\\NClient.Providers.Mapping.LanguageExt.Tests.csproj", + "tests\\NClient.Providers\\NClient.Providers.Caching.Redis.Tests\\NClient.Providers.Caching.Redis.Tests.csproj", "tests\\NClient.Providers\\NClient.Providers.Transport.SystemNetHttp.Tests\\NClient.Providers.Transport.SystemNetHttp.Tests.csproj", "tests\\NClient.Providers\\NClient.Providers.Serialization.SystemTextJson.Tests\\NClient.Providers.Serialization.SystemTextJson.Tests.csproj", "tests\\NClient.Providers\\NClient.Providers.Serialization.SystemXml.Tests\\NClient.Providers.Serialization.SystemXml.Tests.csproj", @@ -22,4 +23,4 @@ "tests\\NClient\\NClient.Tests\\NClient.Tests.csproj" ] } -} \ No newline at end of file +} diff --git a/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/RedisCacheWorkerExtensions.cs b/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/RedisCacheWorkerExtensions.cs new file mode 100644 index 000000000..526ebecf9 --- /dev/null +++ b/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/RedisCacheWorkerExtensions.cs @@ -0,0 +1,24 @@ +using NClient.Common.Helpers; +using NClient.Providers.Caching.Redis; +using StackExchange.Redis; + +// ReSharper disable once CheckNamespace +namespace NClient +{ + public static class RedisCacheWorkerExtensions + { + /// Sets the Redis cache for the client. + public static INClientOptionalBuilder WithRedisCaching( + this INClientOptionalBuilder optionalBuilder, + IDatabaseAsync dataBase) + where TClient : class + { + Ensure.IsNotNull(optionalBuilder, nameof(optionalBuilder)); + + return optionalBuilder.WithAdvancedResponseCaching(selector => + { + selector.ForClient().Use(new RedisCacheWorkerProvider(dataBase)); + }); + } + } +} diff --git a/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/UseRedisCacheWorkerExtensions.cs b/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/UseRedisCacheWorkerExtensions.cs new file mode 100644 index 000000000..ee4e757de --- /dev/null +++ b/src/NClient.Providers/NClient.Providers.Caching.Redis/Extensions/UseRedisCacheWorkerExtensions.cs @@ -0,0 +1,16 @@ +using NClient.Providers.Caching.Redis; +using NClient.Providers.Transport; +using StackExchange.Redis; + +namespace NClient +{ + public static class UseRedisCacheWorkerExtensions + { + /// Sets the cache worker that can store data to Redis. + public static INClientResponseCachingSelector UseRedisCaching( + this INClientTransportResponseCachingSetter optionalBuilder, IDatabaseAsync dataBase) + { + return optionalBuilder.Use(new RedisCacheWorkerProvider(dataBase)); + } + } +} diff --git a/src/NClient.Providers/NClient.Providers.Caching.Redis/NClient.Providers.Caching.Redis.csproj b/src/NClient.Providers/NClient.Providers.Caching.Redis/NClient.Providers.Caching.Redis.csproj new file mode 100644 index 000000000..f7404c897 --- /dev/null +++ b/src/NClient.Providers/NClient.Providers.Caching.Redis/NClient.Providers.Caching.Redis.csproj @@ -0,0 +1,55 @@ + + + + $(AssemblyName) + $(VersionPrefix) + $(VersionSuffix) + logo.png + https://nclient.github.io + https://github.com/nclient/nclient + git + NClient;NClient.Providers;NClient.Providers.Caching;Redis + The provider that allows you to use the Redis for caching NClient requests. + kingmidas74 + Copyright (c) $([System.DateTime]::Now.ToString('yyyy')) NClient + LICENSE + true + + + + netstandard2.0;netstandard2.1 + 9.0 + enable + true + true + Debug;Release + AnyCPU + $(SolutionDir)/bin/src/$(Configuration)/$(AssemblyName) + + + + + + + + + + + + + + Common\Ensure.cs + + + + + + <_Parameter1>NClient.Providers.Caching.Redis.Tests + + + + + + + + diff --git a/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorker.cs b/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorker.cs new file mode 100644 index 000000000..ae9b8bc4a --- /dev/null +++ b/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorker.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NClient.Common.Helpers; +using NClient.Providers.Transport; +using StackExchange.Redis; + +namespace NClient.Providers.Caching.Redis +{ + internal class RedisCacheWorker : IResponseCacheWorker + { + private readonly IDatabaseAsync _redisDb; + private readonly IToolset _toolset; + + public RedisCacheWorker(IDatabaseAsync redisDb, IToolset toolset) + { + Ensure.IsNotNull(redisDb, nameof(redisDb)); + + _redisDb = redisDb; + _toolset = toolset; + } + public async Task FindAsync(IRequest request, CancellationToken cancellationToken = default) + { + Ensure.IsNotNull(request, nameof(request)); + + var serializedResponse = (await _redisDb.StringGetAsync(GenerateKey(request))).ToString(); + + if (string.IsNullOrEmpty(serializedResponse)) + return default; + + return (IResponse) _toolset.Serializer.Deserialize(serializedResponse, typeof(IResponse))!; + } + public async Task PutAsync(IRequest request, IResponse response, TimeSpan? lifeTime = null, CancellationToken cancellationToken = default) + { + Ensure.IsNotNull(request, nameof(request)); + Ensure.IsNotNull(response, nameof(response)); + + if (lifeTime?.TotalMilliseconds <= 0) + return; + + var serializedResponse = _toolset.Serializer.Serialize(response); + + if (!await _redisDb.StringSetAsync(GenerateKey(request), serializedResponse, lifeTime)) + throw new InvalidOperationException("Couldn't save data to Redis"); + } + + private string GenerateKey(IRequest request) + { + Ensure.IsNotNull(request, nameof(request)); + + var key = new StringBuilder(request.Type.ToString()); + key.Append(request.Resource.Scheme); + key.Append(request.Resource.Host); + key.Append(request.Resource.Query); + foreach (var requestMetadata in request.Metadatas) + { + key.Append(requestMetadata.Key); + foreach (var metadata in requestMetadata.Value) + { + key.Append(metadata.Name); + key.Append(metadata.Value); + } + } + var plainTextBytes = Encoding.UTF8.GetBytes(key.ToString()); + return Convert.ToBase64String(plainTextBytes); + } + } +} diff --git a/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorkerProvider.cs b/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorkerProvider.cs new file mode 100644 index 000000000..8ccef580e --- /dev/null +++ b/src/NClient.Providers/NClient.Providers.Caching.Redis/RedisCacheWorkerProvider.cs @@ -0,0 +1,30 @@ +using NClient.Common.Helpers; +using NClient.Providers.Serialization; +using StackExchange.Redis; + +namespace NClient.Providers.Caching.Redis +{ + /// + /// The provider of the cache worker that store HTTP response data to Redis. + /// + public class RedisCacheWorkerProvider : IResponseCacheProvider + { + private readonly IDatabaseAsync _redisDb; + + /// Initializes the Redis cache worker based serializer provider. + /// The redis database. + public RedisCacheWorkerProvider(IDatabaseAsync redisDb) + { + Ensure.IsNotNull(redisDb, nameof(redisDb)); + + _redisDb = redisDb; + } + + /// Creates System.Text.Json instance. + /// Tools that help implement providers. + public IResponseCacheWorker Create(IToolset toolset) + { + return new RedisCacheWorker(_redisDb, toolset); + } + } +} diff --git a/src/NClient/NClient.Abstractions/Annotations/ICachingAttribute.cs b/src/NClient/NClient.Abstractions/Annotations/ICachingAttribute.cs new file mode 100644 index 000000000..1f154379d --- /dev/null +++ b/src/NClient/NClient.Abstractions/Annotations/ICachingAttribute.cs @@ -0,0 +1,9 @@ +namespace NClient.Annotations +{ + /// Identifies an action that should be cached. + public interface ICachingAttribute + { + /// The life time of cache. + double Milliseconds { get; } + } +} diff --git a/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSelector.cs b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSelector.cs new file mode 100644 index 000000000..79c1dc471 --- /dev/null +++ b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSelector.cs @@ -0,0 +1,16 @@ +// ReSharper disable once CheckNamespace + +namespace NClient +{ + /// Selector for configuring mapping on the selected client layer. + /// The type of request that is used in the transport implementation. + /// The type of response that is used in the transport implementation. + public interface INClientResponseCachingSelector + { + /// Select NClient layer. + INClientResponseCachingSetter ForClient(); + + /// Select transport layer. + INClientTransportResponseCachingSetter ForTransport(); + } +} diff --git a/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSetter.cs b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSetter.cs new file mode 100644 index 000000000..37e1ec1d5 --- /dev/null +++ b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientResponseCachingSetter.cs @@ -0,0 +1,19 @@ +using NClient.Providers.Caching; + +// ReSharper disable once CheckNamespace +namespace NClient +{ + /// Setter for custom functionality to mapping NClient requests. + /// The type of request that is used in the transport implementation. + /// The type of response that is used in the transport implementation. + public interface INClientResponseCachingSetter + { + /// Sets a custom mappers that can convert NClient responses into custom ones. + /// The mappers that convert transport responses into custom results. + INClientResponseCachingSelector Use(IResponseCacheWorker cacheWorkers); + + /// Sets a providers creating custom mappers that can convert NClient responses into custom ones. + /// The providers of a mappers that convert transport responses into custom results. + INClientResponseCachingSelector Use(IResponseCacheProvider cacheProvider); + } +} diff --git a/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientTransportResponseCachingSetter.cs b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientTransportResponseCachingSetter.cs new file mode 100644 index 000000000..4908b64d1 --- /dev/null +++ b/src/NClient/NClient.Abstractions/Building/Configuration/Caching/INClientTransportResponseCachingSetter.cs @@ -0,0 +1,19 @@ +using NClient.Providers.Caching; + +// ReSharper disable once CheckNamespace +namespace NClient +{ + /// Setter for custom functionality to mapping transport requests. + /// The type of request that is used in the transport implementation. + /// The type of response that is used in the transport implementation. + public interface INClientTransportResponseCachingSetter + { + /// Sets a custom mappers that can convert NClient responses into custom ones. + /// The mapper that convert transport responses into custom results. + INClientResponseCachingSelector Use(IResponseCacheWorker cacheWorker); + + /// Sets a providers creating custom mappers that can convert NClient responses into custom ones. + /// The provider of a mappers that convert transport responses into custom results. + INClientResponseCachingSelector Use(IResponseCacheProvider cacheProvider); + } +} diff --git a/src/NClient/NClient.Abstractions/Building/INClientOptionalBuilder.cs b/src/NClient/NClient.Abstractions/Building/INClientOptionalBuilder.cs index fb42656d7..2aaec88c9 100644 --- a/src/NClient/NClient.Abstractions/Building/INClientOptionalBuilder.cs +++ b/src/NClient/NClient.Abstractions/Building/INClientOptionalBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; using NClient.Providers.Authorization; +using NClient.Providers.Caching; using NClient.Providers.Handling; using NClient.Providers.Mapping; using NClient.Providers.Serialization; @@ -108,6 +109,17 @@ public interface INClientOptionalBuilder where TCl /// Removes logging of client actions. INClientOptionalBuilder WithoutLogging(); + #endregion + + #region Caching + + /// Sets cache workers that caching NClient responses into custom storage. + /// The collection of cache workers that caching NClient responses into custom storage. + INClientOptionalBuilder WithResponseCaching(IResponseCacheProvider cacheProvider); + + /// Sets advanced cache workers that caching NClient responses into custom storage. + INClientOptionalBuilder WithAdvancedResponseCaching(Action> configure); + #endregion /// Creates instance of . diff --git a/src/NClient/NClient.Abstractions/Extensions/Caching/ExtraCachingExtensions.cs b/src/NClient/NClient.Abstractions/Extensions/Caching/ExtraCachingExtensions.cs new file mode 100644 index 000000000..7e8442df4 --- /dev/null +++ b/src/NClient/NClient.Abstractions/Extensions/Caching/ExtraCachingExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NClient.Annotations; +using NClient.Providers.Caching; +using NClient.Providers.Transport; + +// ReSharper disable once CheckNamespace +namespace NClient +{ + public static class ExtraCachingExtensions + { + /// Sets the mappers that convert NClient responses into custom results. + /// + /// The responseCacheWorker that converts transport responses into custom results. + public static INClientResponseCachingSelector Use( + this INClientResponseCachingSetter responseCachingSetter, + IResponseCacheWorker responseCacheWorker) + { + return responseCachingSetter.Use(responseCacheWorker); + } + + /// Sets the mappers that convert NClient responses into custom results. + /// + /// The responseCacheWorker that converts transport responses into custom results. + public static INClientResponseCachingSelector Use( + this INClientTransportResponseCachingSetter transportResponseCachingSetter, + IResponseCacheWorker responseCacheWorker) + { + return transportResponseCachingSetter.Use(responseCacheWorker); + } + + public static async Task Put(this IResponseCacheWorker? worker, IRequest request, IResponse response, ICachingAttribute? cachingAttribute, CancellationToken cancellationToken = default) + { + if (worker is not null) + await worker.PutAsync(request, response, TimeSpan.FromMilliseconds(cachingAttribute?.Milliseconds ?? 0), cancellationToken); + } + + public static async Task TryGet(this IResponseCacheWorker? worker, IRequest request, CancellationToken cancellationToken = default) + { + if (worker is null) + return null; + return await worker.FindAsync(request, cancellationToken); + } + } +} diff --git a/src/NClient/NClient.Abstractions/Invocation/IMethod.cs b/src/NClient/NClient.Abstractions/Invocation/IMethod.cs index b94f1a60f..d1745f0d0 100644 --- a/src/NClient/NClient.Abstractions/Invocation/IMethod.cs +++ b/src/NClient/NClient.Abstractions/Invocation/IMethod.cs @@ -31,6 +31,9 @@ public interface IMethod /// Get represents time limit for method execution. ITimeoutAttribute? TimeoutAttribute { get; } + /// Get represents time limit for cahing. + ICachingAttribute? CachingAttribute { get; } + /// Get array of represents additional method info (like headers for HTTP). IMetadataAttribute[] MetadataAttributes { get; } diff --git a/src/NClient/NClient.Abstractions/Invocation/Method.cs b/src/NClient/NClient.Abstractions/Invocation/Method.cs index d4baddda9..dfe9130df 100644 --- a/src/NClient/NClient.Abstractions/Invocation/Method.cs +++ b/src/NClient/NClient.Abstractions/Invocation/Method.cs @@ -15,6 +15,7 @@ internal class Method : IMethod public IUseVersionAttribute? UseVersionAttribute { get; set; } public IMetadataAttribute[] MetadataAttributes { get; set; } public ITimeoutAttribute? TimeoutAttribute { get; set; } + public ICachingAttribute? CachingAttribute { get; set; } public IMethodParam[] Params { get; } public Type ResultType { get; } diff --git a/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheProvider.cs b/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheProvider.cs new file mode 100644 index 000000000..039732d5f --- /dev/null +++ b/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheProvider.cs @@ -0,0 +1,12 @@ +namespace NClient.Providers.Caching +{ + /// + /// A provider abstraction for a component that can create instances. + /// + public interface IResponseCacheProvider + { + /// Creates and configures an instance of instance. + /// Tools that help implement providers. + IResponseCacheWorker Create(IToolset toolset); + } +} diff --git a/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheWorker.cs b/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheWorker.cs new file mode 100644 index 000000000..47748bec7 --- /dev/null +++ b/src/NClient/NClient.Abstractions/Providers/Caching/IResponseCacheWorker.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NClient.Providers.Transport; + +namespace NClient.Providers.Caching +{ + /// The worker that manage caching of responses. + public interface IResponseCacheWorker + { + public Task FindAsync(IRequest request, CancellationToken cancellationToken = default); + public Task PutAsync(IRequest request, IResponse response, TimeSpan? lifeTime = null, CancellationToken cancellationToken = default); + } +} diff --git a/src/NClient/NClient.Annotations/CachingAttribute.cs b/src/NClient/NClient.Annotations/CachingAttribute.cs new file mode 100644 index 000000000..51ae65d82 --- /dev/null +++ b/src/NClient/NClient.Annotations/CachingAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace NClient.Annotations +{ + /// Identifies an action that should be cached. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = true)] + public class CachingAttribute : Attribute, ICachingAttribute + { + /// The life time of cache. + public double Milliseconds { get; } + + /// Initializes a new with the given milliseconds value. + /// The life period in milliseconds. + public CachingAttribute(double milliseconds) + { + Milliseconds = milliseconds; + } + } +} diff --git a/src/NClient/NClient.Standalone/Client/Caching/ResponseCacheProvider.cs b/src/NClient/NClient.Standalone/Client/Caching/ResponseCacheProvider.cs new file mode 100644 index 000000000..b5ddd45fb --- /dev/null +++ b/src/NClient/NClient.Standalone/Client/Caching/ResponseCacheProvider.cs @@ -0,0 +1,20 @@ +using NClient.Providers; +using NClient.Providers.Caching; + +namespace NClient.Standalone.Client.Caching +{ + internal class ResponseCacheProvider : IResponseCacheProvider + { + private readonly IResponseCacheWorker _responseCacheWorker; + + public ResponseCacheProvider(IResponseCacheWorker responseCacheWorker) + { + _responseCacheWorker = responseCacheWorker; + } + + public IResponseCacheWorker Create(IToolset toolset) + { + return _responseCacheWorker; + } + } +} diff --git a/src/NClient/NClient.Standalone/Client/TransportNClient.cs b/src/NClient/NClient.Standalone/Client/TransportNClient.cs index 5595418c3..9c7bd2043 100644 --- a/src/NClient/NClient.Standalone/Client/TransportNClient.cs +++ b/src/NClient/NClient.Standalone/Client/TransportNClient.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using NClient.Providers.Caching; using NClient.Common.Helpers; using NClient.Providers.Handling; using NClient.Providers.Mapping; @@ -36,6 +37,7 @@ internal class TransportNClient : ITransportNClient _transportResponseMapper; private readonly IResponseMapper _responseMapper; private readonly IResponseValidator _responseValidator; + private readonly IResponseCacheWorker? _transportResponseCacheWorker; private readonly ILogger _logger; public TimeSpan Timeout => _transport.Timeout; @@ -49,7 +51,8 @@ public TransportNClient( IResiliencePolicy resiliencePolicy, IResponseMapper responseMapper, IResponseMapper transportResponseMapper, - IResponseValidator responseValidator, + IResponseValidator responseValidator, + IResponseCacheWorker? transportResponseCacheWorker, ILogger logger) { _serializer = serializer; @@ -61,6 +64,7 @@ public TransportNClient( _responseMapper = responseMapper; _transportResponseMapper = transportResponseMapper; _responseValidator = responseValidator; + _transportResponseCacheWorker = transportResponseCacheWorker; _logger = logger; } diff --git a/src/NClient/NClient.Standalone/Client/TransportNClientFactory.cs b/src/NClient/NClient.Standalone/Client/TransportNClientFactory.cs index 8af845434..05d5597e5 100644 --- a/src/NClient/NClient.Standalone/Client/TransportNClientFactory.cs +++ b/src/NClient/NClient.Standalone/Client/TransportNClientFactory.cs @@ -1,4 +1,5 @@ -using NClient.Providers; +using NClient.Providers; +using NClient.Providers.Caching; using NClient.Providers.Handling; using NClient.Providers.Mapping; using NClient.Providers.Resilience; @@ -22,6 +23,7 @@ internal class TransportNClientFactory : ITransportNClientF private readonly IResponseMapperProvider _responseMapperProvider; private readonly IResponseMapperProvider _transportResponseMapperProvider; private readonly IResponseValidatorProvider _responseValidatorProvider; + private readonly IResponseCacheProvider? _transportResponseCacheProvider; private readonly IToolset _toolset; public TransportNClientFactory( @@ -33,6 +35,7 @@ public TransportNClientFactory( IResponseMapperProvider responseMapperProvider, IResponseMapperProvider transportResponseMapperProvider, IResponseValidatorProvider responseValidatorProvider, + //IResponseCacheProvider? transportResponseCacheProvider, IToolset toolset) { _transportProvider = transportProvider; @@ -43,6 +46,7 @@ public TransportNClientFactory( _responseMapperProvider = responseMapperProvider; _transportResponseMapperProvider = transportResponseMapperProvider; _responseValidatorProvider = responseValidatorProvider; + //_transportResponseCacheProvider = transportResponseCacheProvider; _toolset = toolset; } @@ -58,6 +62,7 @@ public ITransportNClient Create() _responseMapperProvider.Create(_toolset), _transportResponseMapperProvider.Create(_toolset), _responseValidatorProvider.Create(_toolset), + _transportResponseCacheProvider?.Create(_toolset), _toolset.Logger); } } diff --git a/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSelector.cs b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSelector.cs new file mode 100644 index 000000000..ba0be9590 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSelector.cs @@ -0,0 +1,24 @@ +using NClient.Standalone.ClientProxy.Building.Context; + +namespace NClient.Standalone.ClientProxy.Building.Configuration.Caching +{ + internal class NClientResponseCachingSelector : INClientResponseCachingSelector + { + private readonly BuilderContextModifier _builderContextModifier; + + public NClientResponseCachingSelector(BuilderContextModifier builderContextModifier) + { + _builderContextModifier = builderContextModifier; + } + + public INClientResponseCachingSetter ForClient() + { + return new NClientResponseCachingSetter(_builderContextModifier); + } + + public INClientTransportResponseCachingSetter ForTransport() + { + return new NClientTransportResponseCachingSetter(_builderContextModifier); + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSetter.cs b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSetter.cs new file mode 100644 index 000000000..6e1bc0703 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientResponseCachingSetter.cs @@ -0,0 +1,32 @@ +using NClient.Common.Helpers; +using NClient.Providers.Caching; +using NClient.Standalone.Client.Caching; +using NClient.Standalone.ClientProxy.Building.Context; + +namespace NClient.Standalone.ClientProxy.Building.Configuration.Caching +{ + internal class NClientResponseCachingSetter : INClientResponseCachingSetter + { + private readonly BuilderContextModifier _builderContextModifier; + + public NClientResponseCachingSetter(BuilderContextModifier builderContextModifier) + { + _builderContextModifier = builderContextModifier; + } + + public INClientResponseCachingSelector Use(IResponseCacheWorker cacheWorker) + { + Ensure.IsNotNull(cacheWorker, nameof(cacheWorker)); + + return Use(new ResponseCacheProvider(cacheWorker)); + } + + public INClientResponseCachingSelector Use(IResponseCacheProvider cacheProvider) + { + Ensure.IsNotNull(cacheProvider, nameof(cacheProvider)); + + _builderContextModifier.Add(x => x.WithResponseCachingProvider(cacheProvider)); + return new NClientResponseCachingSelector(_builderContextModifier); + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientTransportResponseCachingSetter.cs b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientTransportResponseCachingSetter.cs new file mode 100644 index 000000000..292c93557 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Building/Configuration/Caching/NClientTransportResponseCachingSetter.cs @@ -0,0 +1,32 @@ +using NClient.Common.Helpers; +using NClient.Providers.Caching; +using NClient.Standalone.Client.Caching; +using NClient.Standalone.ClientProxy.Building.Context; + +namespace NClient.Standalone.ClientProxy.Building.Configuration.Caching +{ + internal class NClientTransportResponseCachingSetter : INClientTransportResponseCachingSetter + { + private readonly BuilderContextModifier _builderContextModifier; + + public NClientTransportResponseCachingSetter(BuilderContextModifier builderContextModifier) + { + _builderContextModifier = builderContextModifier; + } + + public INClientResponseCachingSelector Use(IResponseCacheWorker cacheWorker) + { + Ensure.IsNotNull(cacheWorker, nameof(cacheWorker)); + + return Use(new ResponseCacheProvider(cacheWorker)); + } + + public INClientResponseCachingSelector Use(IResponseCacheProvider cacheProvider) + { + Ensure.IsNotNull(cacheProvider, nameof(cacheProvider)); + + _builderContextModifier.Add(x => x.WithTransportCachingProvider(cacheProvider)); + return new NClientResponseCachingSelector(_builderContextModifier); + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Building/Context/BuilderContext.cs b/src/NClient/NClient.Standalone/ClientProxy/Building/Context/BuilderContext.cs index 88bb6b6db..14f6a0a8f 100644 --- a/src/NClient/NClient.Standalone/ClientProxy/Building/Context/BuilderContext.cs +++ b/src/NClient/NClient.Standalone/ClientProxy/Building/Context/BuilderContext.cs @@ -5,6 +5,7 @@ using NClient.Invocation; using NClient.Providers.Api; using NClient.Providers.Authorization; +using NClient.Providers.Caching; using NClient.Providers.Handling; using NClient.Providers.Mapping; using NClient.Providers.Resilience; @@ -41,6 +42,8 @@ internal class BuilderContext public IReadOnlyCollection> ResponseMapperProviders { get; private set; } public IReadOnlyCollection> TransportResponseMapperProviders { get; private set; } + public IResponseCacheProvider? CacheProvider { get; private set; } + public IResponseCacheProvider? TransportCacheProvider { get; private set; } public TimeSpan? Timeout { get; private set; } @@ -84,6 +87,9 @@ public BuilderContext(BuilderContext builderContext) ResponseMapperProviders = builderContext.ResponseMapperProviders.ToArray(); TransportResponseMapperProviders = builderContext.TransportResponseMapperProviders.ToArray(); + + CacheProvider = builderContext.CacheProvider; + TransportCacheProvider = builderContext.TransportCacheProvider; Timeout = builderContext.Timeout; @@ -270,6 +276,22 @@ public BuilderContext WithoutLogging() LoggerFactory = null }; } + + public BuilderContext WithResponseCachingProvider(IResponseCacheProvider responseCacheProvider) + { + return new BuilderContext(this) + { + CacheProvider = responseCacheProvider + }; + } + + public BuilderContext WithTransportCachingProvider(IResponseCacheProvider transportResponseCacheProvider) + { + return new BuilderContext(this) + { + TransportCacheProvider = transportResponseCacheProvider + }; + } public void EnsureComplete() { diff --git a/src/NClient/NClient.Standalone/ClientProxy/Building/NClientOptionalBuilder.cs b/src/NClient/NClient.Standalone/ClientProxy/Building/NClientOptionalBuilder.cs index bf47a3777..b293bb5c5 100644 --- a/src/NClient/NClient.Standalone/ClientProxy/Building/NClientOptionalBuilder.cs +++ b/src/NClient/NClient.Standalone/ClientProxy/Building/NClientOptionalBuilder.cs @@ -5,11 +5,13 @@ using NClient.Common.Helpers; using NClient.Core.Proxy; using NClient.Providers.Authorization; +using NClient.Providers.Caching; using NClient.Providers.Handling; using NClient.Providers.Mapping; using NClient.Providers.Serialization; using NClient.Providers.Validation; using NClient.Standalone.Client.Authorization; +using NClient.Standalone.ClientProxy.Building.Configuration.Caching; using NClient.Standalone.ClientProxy.Building.Configuration.Handling; using NClient.Standalone.ClientProxy.Building.Configuration.Mapping; using NClient.Standalone.ClientProxy.Building.Configuration.Resilience; @@ -167,14 +169,29 @@ public INClientOptionalBuilder WithoutLogging() return new NClientOptionalBuilder(_context .WithoutLogging()); } + + public INClientOptionalBuilder WithResponseCaching(IResponseCacheProvider cacheProvider) + { + return WithAdvancedResponseCaching(x => x + .ForTransport().Use(cacheProvider)); + } + + public INClientOptionalBuilder WithAdvancedResponseCaching(Action> configure) + { + Ensure.IsNotNull(configure, nameof(configure)); + + var builderContextModifier = new BuilderContextModifier(); + configure(new NClientResponseCachingSelector(builderContextModifier)); + return new NClientOptionalBuilder(builderContextModifier.Invoke(_context)); + } public TClient Build() { _context.EnsureComplete(); - new ClientValidator(_proxyGeneratorProvider.Value) + /*new ClientValidator(_proxyGeneratorProvider.Value) .EnsureAsync(_clientInterceptorFactory) .GetAwaiter() - .GetResult(); + .GetResult();*/ var interceptor = _clientInterceptorFactory.Create(_context); return _clientProxyGenerator.CreateClient(interceptor); diff --git a/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptor.cs b/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptor.cs index 58cf89650..74d4b9946 100644 --- a/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptor.cs +++ b/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptor.cs @@ -5,10 +5,12 @@ using System.Threading.Tasks; using Castle.DynamicProxy; using Microsoft.Extensions.Logging; +using NClient.Annotations; using NClient.Core.Helpers; using NClient.Exceptions; using NClient.Providers; using NClient.Providers.Api; +using NClient.Providers.Caching; using NClient.Providers.Authorization; using NClient.Providers.Resilience; using NClient.Providers.Transport; @@ -34,6 +36,7 @@ internal class ClientInterceptor : AsyncIntercepto private readonly ITransportNClientFactory _transportNClientFactory; private readonly IMethodResiliencePolicyProvider _methodResiliencePolicyProvider; private readonly IClientRequestExceptionFactory _clientRequestExceptionFactory; + private readonly IResponseCacheWorker? _responseCacheWorker; private readonly TimeSpan? _timeout; private readonly IToolset _toolset; @@ -48,7 +51,8 @@ public ClientInterceptor( IRequestBuilderProvider requestBuilderProvider, ITransportNClientFactory transportNClientFactory, IMethodResiliencePolicyProvider methodResiliencePolicyProvider, - IClientRequestExceptionFactory clientRequestExceptionFactory, + IClientRequestExceptionFactory clientRequestExceptionFactory, + IResponseCacheWorker? responseCacheWorker, TimeSpan? timeout, IToolset toolset) { @@ -63,6 +67,7 @@ public ClientInterceptor( _transportNClientFactory = transportNClientFactory; _methodResiliencePolicyProvider = methodResiliencePolicyProvider; _clientRequestExceptionFactory = clientRequestExceptionFactory; + _responseCacheWorker = responseCacheWorker; _timeout = timeout; _toolset = toolset; } @@ -128,8 +133,8 @@ protected override async Task InterceptAsync( var resiliencePolicy = methodInvocation.ResiliencePolicyProvider?.Create(_toolset) ?? _methodResiliencePolicyProvider.Create(methodInvocation.Method, request, _toolset); - - var result = await ExecuteHttpResponseAsync(transportNClient, request, resultType, resiliencePolicy, combinedCancellationToken).ConfigureAwait(false); + + var result = await ExecuteHttpResponseAsync(transportNClient, request, resultType, resiliencePolicy, method.CachingAttribute, combinedCancellationToken).ConfigureAwait(false); _toolset.Logger?.LogDebug("Processing request finished"); return result; } @@ -164,33 +169,59 @@ protected override async Task InterceptAsync( } } - private static async Task ExecuteHttpResponseAsync(ITransportNClient transportNClient, IRequest request, Type resultType, IResiliencePolicy? resiliencePolicy, CancellationToken cancellationToken) + private async Task ExecuteHttpResponseAsync(ITransportNClient transportNClient, IRequest request, Type resultType, IResiliencePolicy? resiliencePolicy, ICachingAttribute? cachingAttribute = default, CancellationToken cancellationToken = default) { + if (await _responseCacheWorker.TryGet(request, cancellationToken) is { } cachedResult) + { + _toolset.Logger?.LogDebug("Response received from cache. Request id: '{requestId}'.", request.Id); + return cachedResult; + } + if (resultType == typeof(TResponse)) return await transportNClient .GetTransportResponseAsync(request, resiliencePolicy, cancellationToken) .ConfigureAwait(false); - + if (resultType == typeof(IResponse)) - return await transportNClient + { + var result = await transportNClient .GetResponseAsync(request, resiliencePolicy, cancellationToken) .ConfigureAwait(false); - + + await _responseCacheWorker.Put(request, result, cachingAttribute, cancellationToken); + return result; + } + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(IResponse<>).GetGenericTypeDefinition()) - return await transportNClient + { + var result = await transportNClient .GetResponseWithDataAsync(request, dataType: resultType.GetGenericArguments().Single(), resiliencePolicy, cancellationToken) .ConfigureAwait(false); - + + await _responseCacheWorker.Put(request, result, cachingAttribute, cancellationToken); + return result; + } + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(IResponseWithError<>).GetGenericTypeDefinition()) - return await transportNClient + { + var result = await transportNClient .GetResponseWithErrorAsync(request, errorType: resultType.GetGenericArguments().Single(), resiliencePolicy, cancellationToken) .ConfigureAwait(false); - + + await _responseCacheWorker.Put(request, result, cachingAttribute, cancellationToken); + return result; + } + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(IResponseWithError<,>).GetGenericTypeDefinition()) - return await transportNClient + { + var result = await transportNClient .GetResponseWithDataOrErrorAsync(request, dataType: resultType.GetGenericArguments()[0], errorType: resultType.GetGenericArguments()[1], resiliencePolicy, cancellationToken) .ConfigureAwait(false); - + + await _responseCacheWorker.Put(request, result, cachingAttribute, cancellationToken); + return result; + } + if (resultType != typeof(void)) return await transportNClient .GetResultAsync(request, resultType, resiliencePolicy, cancellationToken) @@ -199,6 +230,7 @@ protected override async Task InterceptAsync( await transportNClient .GetResultAsync(request, resiliencePolicy, cancellationToken) .ConfigureAwait(false); + return null; } } diff --git a/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptorFactory.cs b/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptorFactory.cs index 0ff721d83..7db75c68d 100644 --- a/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptorFactory.cs +++ b/src/NClient/NClient.Standalone/ClientProxy/Generation/Interceptors/ClientInterceptorFactory.cs @@ -51,6 +51,7 @@ public ClientInterceptorFactory(IProxyGenerator proxyGenerator) new PathAttributeProvider(attributeMapper, clientValidationExceptionFactory), new MetadataAttributeProvider(clientValidationExceptionFactory), new TimeoutAttributeProvider(attributeMapper, clientValidationExceptionFactory), + new CachingAttributeProvider(attributeMapper, clientValidationExceptionFactory), new MethodParamBuilder(new ParamAttributeProvider(attributeMapper, clientValidationExceptionFactory))); } @@ -70,10 +71,11 @@ public IAsyncInterceptor Create(BuilderContext(builderContext.ResponseMapperProviders); var transportResponseMapperProvider = new CompositeResponseMapperProvider(builderContext.TransportResponseMapperProviders); var compositeResponseValidatorProvider = new CompositeResponseValidatorProvider(builderContext.ResponseValidatorProviders); + var responseCacheWorker = builderContext.CacheProvider?.Create(toolset); var methodResiliencePolicyProviderAdapter = new MethodResiliencePolicyProviderAdapter( new StubResiliencePolicyProvider(), builderContext.MethodsWithResiliencePolicy.Reverse()); - + return new ClientInterceptor( builderContext.Host, _timeoutSelector, @@ -95,6 +97,7 @@ public IAsyncInterceptor Create(BuilderContext(); @@ -41,6 +43,7 @@ public MethodBuilder( _pathAttributeProvider = pathAttributeProvider; _metadataAttributeProvider = metadataAttributeProvider; _timeoutAttributeProvider = timeoutAttributeProvider; + _cachingAttributeProvider = cachingAttributeProvider; _methodParamBuilder = methodParamBuilder; } @@ -76,7 +79,8 @@ public IMethod Build(Type clientType, MethodInfo methodInfo, Type returnType) PathAttribute = _pathAttributeProvider.Find(clientType), UseVersionAttribute = _useVersionAttributeProvider.Find(clientType, methodInfo, overridingMethods), MetadataAttributes = _metadataAttributeProvider.Find(clientType, methodInfo, overridingMethods, methodParams), - TimeoutAttribute = _timeoutAttributeProvider.Find(clientType, methodInfo, overridingMethods) + TimeoutAttribute = _timeoutAttributeProvider.Find(clientType, methodInfo, overridingMethods), + CachingAttribute = _cachingAttributeProvider.Find(clientType, methodInfo, overridingMethods) }; _cache.TryAdd(methodInfo, method); diff --git a/src/NClient/NClient.Standalone/ClientProxy/Generation/MethodBuilders/Providers/CachingAttributeProvider.cs b/src/NClient/NClient.Standalone/ClientProxy/Generation/MethodBuilders/Providers/CachingAttributeProvider.cs new file mode 100644 index 000000000..80c3bc335 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Generation/MethodBuilders/Providers/CachingAttributeProvider.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using NClient.Annotations; +using NClient.Core.Helpers; +using NClient.Core.Mappers; +using NClient.Standalone.Exceptions.Factories; + +namespace NClient.Standalone.ClientProxy.Generation.MethodBuilders.Providers +{ + internal interface ICachingAttributeProvider + { + ICachingAttribute? Find(Type clientType, MethodInfo method, IEnumerable overridingMethods); + } + + internal class CachingAttributeProvider : ICachingAttributeProvider + { + private readonly IAttributeMapper _attributeMapper; + private readonly IClientValidationExceptionFactory _clientValidationExceptionFactory; + + public CachingAttributeProvider( + IAttributeMapper attributeMapper, + IClientValidationExceptionFactory clientValidationExceptionFactory) + { + _attributeMapper = attributeMapper; + _clientValidationExceptionFactory = clientValidationExceptionFactory; + } + + public ICachingAttribute? Find(Type clientType, MethodInfo method, IEnumerable overridingMethods) + { + return Find(clientType, method) ?? overridingMethods.Select(x => Find(clientType, x)).FirstOrDefault(); + } + + private CachingAttribute? Find(Type clientType, MethodInfo method) + { + var typeTimeoutAttribute = FindInType(clientType); + var methodTimeoutAttribute = FindInMethod(method); + + return methodTimeoutAttribute ?? typeTimeoutAttribute; + } + + private CachingAttribute? FindInType(Type clientType) + { + var timeoutAttributes = (clientType.IsInterface + ? clientType.GetInterfaceCustomAttributes(inherit: true) + : clientType.GetCustomAttributes(inherit: true).Cast()) + .Select(x => _attributeMapper.TryMap(x)) + .Where(x => x is CachingAttribute) + .Cast() + .ToArray(); + + if (timeoutAttributes.Length > 1) + throw _clientValidationExceptionFactory.MultipleMethodAttributeNotSupported(); + + return timeoutAttributes.SingleOrDefault(); + } + + private CachingAttribute? FindInMethod(MethodInfo method) + { + var timeoutAttributes = method.GetCustomAttributes() + .Select(x => _attributeMapper.TryMap(x)) + .Where(x => x is CachingAttribute) + .Cast() + .ToArray(); + + if (timeoutAttributes.Length > 1) + throw _clientValidationExceptionFactory.MultipleMethodAttributeNotSupported(); + + return timeoutAttributes.SingleOrDefault(); + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheProvider.cs b/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheProvider.cs new file mode 100644 index 000000000..557bee3b1 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheProvider.cs @@ -0,0 +1,13 @@ +using NClient.Providers; +using NClient.Providers.Caching; + +namespace NClient.Standalone.ClientProxy.Validation.Caching +{ + internal class StubResponseCacheProvider : IResponseCacheProvider + { + public IResponseCacheWorker Create(IToolset toolset) + { + return new StubResponseCacheWorker(); + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheWorker.cs b/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheWorker.cs new file mode 100644 index 000000000..3968503e9 --- /dev/null +++ b/src/NClient/NClient.Standalone/ClientProxy/Validation/Caching/StubResponseCacheWorker.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NClient.Providers.Caching; +using NClient.Providers.Transport; + +namespace NClient.Standalone.ClientProxy.Validation.Caching +{ + internal class StubResponseCacheWorker : IResponseCacheWorker + { + public async Task FindAsync(IRequest request, CancellationToken cancellationToken = default) + { + return await Task.FromResult(default); + } + public async Task PutAsync(IRequest request, IResponse response, TimeSpan? lifeTime = null, CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + } + } +} diff --git a/src/NClient/NClient.Standalone/ClientProxy/Validation/ClientValidator.cs b/src/NClient/NClient.Standalone/ClientProxy/Validation/ClientValidator.cs index 716f1f97c..426551df2 100644 --- a/src/NClient/NClient.Standalone/ClientProxy/Validation/ClientValidator.cs +++ b/src/NClient/NClient.Standalone/ClientProxy/Validation/ClientValidator.cs @@ -6,12 +6,14 @@ using NClient.Exceptions; using NClient.Providers.Mapping; using NClient.Providers.Transport; +using NClient.Standalone.Client.Caching; using NClient.Standalone.Client.Resilience; using NClient.Standalone.ClientProxy.Building.Context; using NClient.Standalone.ClientProxy.Generation; using NClient.Standalone.ClientProxy.Generation.Interceptors; using NClient.Standalone.ClientProxy.Validation.Api; using NClient.Standalone.ClientProxy.Validation.Authorization; +using NClient.Standalone.ClientProxy.Validation.Caching; using NClient.Standalone.ClientProxy.Validation.Handling; using NClient.Standalone.ClientProxy.Validation.Resilience; using NClient.Standalone.ClientProxy.Validation.Serialization; @@ -51,7 +53,9 @@ public ClientValidator(IProxyGenerator proxyGenerator) new StubResiliencePolicyProvider())) .WithResponseMapperProviders(Array.Empty>()) .WithTransportResponseMapperProviders(Array.Empty>()) - .WithResponseValidation(new[] { new StubResponseValidatorProvider() }); + .WithResponseValidation(new[] { new StubResponseValidatorProvider() }) + .WithResponseCachingProvider(new StubResponseCacheProvider()) + .WithTransportCachingProvider(new StubResponseCacheProvider()); } public async Task EnsureAsync(IClientInterceptorFactory clientInterceptorFactory) diff --git a/tests/NClient.Packages/NClient.Packages.Tests/VersionTest.cs b/tests/NClient.Packages/NClient.Packages.Tests/VersionTest.cs index f38822e9c..869e0eb22 100644 --- a/tests/NClient.Packages/NClient.Packages.Tests/VersionTest.cs +++ b/tests/NClient.Packages/NClient.Packages.Tests/VersionTest.cs @@ -43,6 +43,10 @@ public void NClient_Providers_Resilience_Polly() => [Test] public void NClient_Providers_Mapping_HttpResponses() => PackagesVersionProvider.GetCurrent("NClient.Providers.Mapping.HttpResponses").Should().Be(PackagesVersionProvider.GetNew()); + + [Test] + public void NClient_Providers_Caching_Redis() => + PackagesVersionProvider.GetCurrent("NClient.Providers.Caching.Redis").Should().Be(PackagesVersionProvider.GetNew()); [Test] public void NClient_Providers_Mapping_LanguageExt() => diff --git a/tests/NClient.Providers/NClient.Providers.Api.Rest.Tests/RequestBuilderTestBase.cs b/tests/NClient.Providers/NClient.Providers.Api.Rest.Tests/RequestBuilderTestBase.cs index 17943e56b..0cafae97e 100644 --- a/tests/NClient.Providers/NClient.Providers.Api.Rest.Tests/RequestBuilderTestBase.cs +++ b/tests/NClient.Providers/NClient.Providers.Api.Rest.Tests/RequestBuilderTestBase.cs @@ -69,6 +69,7 @@ public void OneTimeSetUp() new PathAttributeProvider(attributeMapper, ClientValidationExceptionFactory), new MetadataAttributeProvider(ClientValidationExceptionFactory), new TimeoutAttributeProvider(attributeMapper, ClientValidationExceptionFactory), + new CachingAttributeProvider(attributeMapper, ClientValidationExceptionFactory), new MethodParamBuilder(new ParamAttributeProvider(attributeMapper, ClientValidationExceptionFactory))); var authorizationMock = new Mock(); diff --git a/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/NClient.Providers.Caching.Redis.Tests.csproj b/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/NClient.Providers.Caching.Redis.Tests.csproj new file mode 100644 index 000000000..cb614dc71 --- /dev/null +++ b/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/NClient.Providers.Caching.Redis.Tests.csproj @@ -0,0 +1,31 @@ + + + + net462;net472;net48;net5.0;net6.0;netcoreapp3.1 + 9.0 + enable + true + true + false + true + Debug;Release + AnyCPU + $(SolutionDir)/bin/test/$(Configuration)/$(AssemblyName) + + + + + + + + + + + + + + + + + + diff --git a/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/RedisCacheWorkerTest.cs b/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/RedisCacheWorkerTest.cs new file mode 100644 index 000000000..d9f80e36a --- /dev/null +++ b/tests/NClient.Providers/NClient.Providers.Caching.Redis.Tests/RedisCacheWorkerTest.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Text.Json; +using FluentAssertions; +using Moq; +using NClient.Testing.Common.Apis; +using NClient.Testing.Common.Clients; +using NClient.Testing.Common.Entities; +using NUnit.Framework; +using StackExchange.Redis; + +namespace NClient.Providers.Caching.Redis.Tests +{ + public class RedisCacheWorkerTest + { + [Test] + public void Caching_RedisCaching_NotThrow() + { + const int id = 1; + var entity = new BasicEntity { Id = 1, Value = 2 }; + + var dbMock = new Mock(); + dbMock.Setup(x => x.StringGetAsync(It.IsAny(), CommandFlags.None)).ReturnsAsync(It.IsAny()); + dbMock.Setup(x => x.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), When.Always, CommandFlags.None)).ReturnsAsync(true); + + using var api = CachingApiMockFactory.MockGetAsyncMethod(id, entity); + + var result = NClientGallery.Clients.GetRest().For(host: api.Urls.First()) + .WithSystemTextJsonSerialization() + .WithRedisCaching(dbMock.Object) + .Build() + .GetIResponse(id); + + result.IsSuccessful.Should().BeTrue(); + result.Data.Should().BeEquivalentTo(entity); + + dbMock.Verify(mock => mock.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), When.Always, CommandFlags.None), Times.Once()); + } + } +} diff --git a/tests/NClient.Testing/NClient.Testing.Common/Apis/CachingApiMockFactory.cs b/tests/NClient.Testing/NClient.Testing.Common/Apis/CachingApiMockFactory.cs new file mode 100644 index 000000000..66859423e --- /dev/null +++ b/tests/NClient.Testing/NClient.Testing.Common/Apis/CachingApiMockFactory.cs @@ -0,0 +1,27 @@ +using NClient.Testing.Common.Entities; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace NClient.Testing.Common.Apis +{ + public class CachingApiMockFactory + { + public static IWireMockServer MockGetAsyncMethod(int id, BasicEntity entity) + { + var api = WireMockServer.Start(); + api.Given(Request.Create() + .WithPath("/api/caching") + .WithHeader("Accept", "application/json") + .WithParam("id", id.ToString()) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Encoding", "utf-8") + .WithHeader("Content-Type", "application/json") + .WithBodyAsJson(entity)); + + return api; + } + } +} diff --git a/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClient.cs b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClient.cs new file mode 100644 index 000000000..d01078b60 --- /dev/null +++ b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClient.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace NClient.Testing.Common.Clients +{ + public interface ICachingClient : INClient + { + int Get(int id); + Task GetAsync(int id); + } +} diff --git a/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClientWithMetadata.cs b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClientWithMetadata.cs new file mode 100644 index 000000000..d1944e69f --- /dev/null +++ b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingClientWithMetadata.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using NClient.Annotations; +using NClient.Annotations.Http; + +namespace NClient.Testing.Common.Clients +{ + [Path("api/caching")] + public interface ICachingClientWithMetadata : ITimeoutClient + { + [GetMethod] + new int Get(int id); + + [GetMethod] + new Task GetAsync(int id); + } +} diff --git a/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingStaticClientWithMetadata.cs b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingStaticClientWithMetadata.cs new file mode 100644 index 000000000..f6e27062a --- /dev/null +++ b/tests/NClient.Testing/NClient.Testing.Common/Clients/ICachingStaticClientWithMetadata.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using NClient.Annotations; +using NClient.Annotations.Http; +using NClient.Providers.Transport; +using NClient.Testing.Common.Entities; + +namespace NClient.Testing.Common.Clients +{ + [Path("api/caching")] + [Caching(500)] + public interface ICachingStaticClientWithMetadata : ITimeoutClient + { + [GetMethod] + [Caching(1000)] + IResponse GetIResponse(int id); + + [GetMethod] + Task> GetIResponseAsync(int id); + } +} diff --git a/tests/NClient/NClient.Standalone.Tests/MethodBuilders/MethodBuilderTest.cs b/tests/NClient/NClient.Standalone.Tests/MethodBuilders/MethodBuilderTest.cs index 02e82549a..73ff3d3e8 100644 --- a/tests/NClient/NClient.Standalone.Tests/MethodBuilders/MethodBuilderTest.cs +++ b/tests/NClient/NClient.Standalone.Tests/MethodBuilders/MethodBuilderTest.cs @@ -31,6 +31,7 @@ public void Build_BasicClient_Method() var useVersionAttribute = (UseVersionAttribute) null!; var pathAttribute = (PathAttribute) null!; var timeoutAttribute = (TimeoutAttribute) null!; + var cachingAttribute = (CachingAttribute) null!; var headerAttributes = Array.Empty(); var methodParams = Array.Empty(); var methodAttributeProviderMock = new Mock(); @@ -45,6 +46,9 @@ public void Build_BasicClient_Method() var timeoutAttributeProviderMock = new Mock(); timeoutAttributeProviderMock.Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(timeoutAttribute); + var cachingAttributeProviderMock = new Mock(); + cachingAttributeProviderMock.Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(cachingAttribute); var headerAttributeProviderMock = new Mock(); headerAttributeProviderMock.Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Returns(headerAttributes); @@ -57,6 +61,7 @@ public void Build_BasicClient_Method() pathAttributeProviderMock.Object, headerAttributeProviderMock.Object, timeoutAttributeProviderMock.Object, + cachingAttributeProviderMock.Object, methodParamBuilderMock.Object); var actualResult = methodBuilder.Build(clientType, methodInfo, returnType);