diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f4c9e23..ef7049cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,5 +44,5 @@ jobs: run: | for file in nugetpkgs/*.nupkg do - dotnet nuget push $file -k ${{ secrets.MYGET_API_KEY }} --skip-duplicate -s https://www.myget.org/F/ncc/api/v2/package + dotnet nuget push $file -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate -s https://www.nuget.org/api/v2/package done \ No newline at end of file diff --git a/EasyCaching.sln b/EasyCaching.sln index a595298f..ff571815 100644 --- a/EasyCaching.sln +++ b/EasyCaching.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29324.140 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A0F5CC7E-155F-4726-8DEB-E966950B3FE9}" EndProject @@ -70,6 +70,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.LiteDB", "src\E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.Serialization.SystemTextJson", "serialization\EasyCaching.Serialization.SystemTextJson\EasyCaching.Serialization.SystemTextJson.csproj", "{4FCF16BF-5E21-4B74-AB45-3C121ADF1485}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyCaching.Bus.ConfluentKafka", "bus\EasyCaching.Bus.ConfluentKafka\EasyCaching.Bus.ConfluentKafka.csproj", "{F7FBADEB-D766-4595-949A-07104B52692C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyCaching.Bus.Zookeeper", "bus\EasyCaching.Bus.Zookeeper\EasyCaching.Bus.Zookeeper.csproj", "{5E488583-391E-4E15-83C1-7301B4FE79AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -184,6 +188,14 @@ Global {4FCF16BF-5E21-4B74-AB45-3C121ADF1485}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FCF16BF-5E21-4B74-AB45-3C121ADF1485}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FCF16BF-5E21-4B74-AB45-3C121ADF1485}.Release|Any CPU.Build.0 = Release|Any CPU + {F7FBADEB-D766-4595-949A-07104B52692C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7FBADEB-D766-4595-949A-07104B52692C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7FBADEB-D766-4595-949A-07104B52692C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7FBADEB-D766-4595-949A-07104B52692C}.Release|Any CPU.Build.0 = Release|Any CPU + {5E488583-391E-4E15-83C1-7301B4FE79AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E488583-391E-4E15-83C1-7301B4FE79AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E488583-391E-4E15-83C1-7301B4FE79AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E488583-391E-4E15-83C1-7301B4FE79AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -216,6 +228,8 @@ Global {711603E1-8363-4F8D-9AA9-8C03EC8BD35F} = {B4241D34-A973-4A13-BD89-9BAE3F2BDDF6} {BA850294-3103-4540-8A27-FC768E1DC8FC} = {A0F5CC7E-155F-4726-8DEB-E966950B3FE9} {4FCF16BF-5E21-4B74-AB45-3C121ADF1485} = {15070C49-A507-4844-BCFE-D319CFBC9A63} + {F7FBADEB-D766-4595-949A-07104B52692C} = {B337509B-75F9-4851-821F-9BBE87C4E4BC} + {5E488583-391E-4E15-83C1-7301B4FE79AE} = {B337509B-75F9-4851-821F-9BBE87C4E4BC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63A57886-054B-476C-AAE1-8D7C8917682E} diff --git a/build/releasenotes.props b/build/releasenotes.props index 2eff3fe0..f2f98711 100644 --- a/build/releasenotes.props +++ b/build/releasenotes.props @@ -1,23 +1,27 @@ - 1. Add CancellationToken for async methods. + 1. Remove BinaryFormatter. + 2. Support removing cache keys by pattern. - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. - 1. Add CancellationToken for async methods. - 2. Add persist key method to redis providers. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. 1. Upgrading dependencies. @@ -38,11 +42,12 @@ 1. Upgrading dependencies. - 1. Add CancellationToken for async methods. - 2. Add persist key method to redis providers. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. 1. Upgrading dependencies. @@ -50,11 +55,19 @@ 1. Upgrading dependencies. + + 1. Add EasyCachingKafkaBus. + + + 1. Add EasyCachingZookeeperBus. + - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. - 1. Add CancellationToken for async methods. + 1. Upgrading dependencies. + 2. Support removing cache keys by pattern. 1. Upgrading dependencies. diff --git a/build/version.props b/build/version.props index 0bb59cda..dee60423 100644 --- a/build/version.props +++ b/build/version.props @@ -1,24 +1,26 @@ - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 - 1.6.1 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 + 1.7.0 diff --git a/bus/EasyCaching.Bus.CSRedis/Configurations/CSRedisOptionsExtension.cs b/bus/EasyCaching.Bus.CSRedis/Configurations/CSRedisOptionsExtension.cs index b56920af..7dcffe0d 100644 --- a/bus/EasyCaching.Bus.CSRedis/Configurations/CSRedisOptionsExtension.cs +++ b/bus/EasyCaching.Bus.CSRedis/Configurations/CSRedisOptionsExtension.cs @@ -41,8 +41,6 @@ public void AddServices(IServiceCollection services) { services.AddOptions(); - services.TryAddSingleton(); - services.Configure(_name, _configure); services.AddSingleton(x => diff --git a/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaBusOptions.cs b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaBusOptions.cs new file mode 100644 index 00000000..27c35007 --- /dev/null +++ b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaBusOptions.cs @@ -0,0 +1,39 @@ +using Confluent.Kafka; + +namespace EasyCaching.Bus.ConfluentKafka +{ + /// + /// kafka bus options + /// + public class ConfluentKafkaBusOptions + { + /// + /// kafka address(BootstrapServers must) + /// + public string BootstrapServers { get; set; } + + + /// + /// kafka bus producer options. + /// + public ProducerConfig ProducerConfig { get; set; } + + /// + /// kafka bus consumer options.(if GroupId value below is empty,then ConsumerConfig.GroupId must ) + /// + public ConsumerConfig ConsumerConfig { get; set; } + + /// + /// kafka bus consumer options with consumer groupId + /// (if ConsumerConfig below has give GroupId value , this options can ignore) + /// import:if application is cluster,you should set this different value in application,this will make consumer can consumerdata + /// + public string GroupId { get; set; } + + /// + /// kafka bus consumer consume count + /// + public int ConsumerCount { get; set; } = 1; + } + +} diff --git a/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaOptionsExtension.cs b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaOptionsExtension.cs new file mode 100644 index 00000000..a810bbfb --- /dev/null +++ b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/ConfluentKafkaOptionsExtension.cs @@ -0,0 +1,43 @@ +namespace EasyCaching.Bus.ConfluentKafka +{ + using System; + using EasyCaching.Core.Bus; + using EasyCaching.Core.Configurations; + using EasyCaching.Core.Serialization; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + + /// + /// Kafka options extension. + /// + internal sealed class ConfluentKafkaOptionsExtension : IEasyCachingOptionsExtension + { + + private readonly Action _confluentKafkaBusOptions; + + public ConfluentKafkaOptionsExtension(Action confluentKafkaBusOptions) + { + this._confluentKafkaBusOptions = confluentKafkaBusOptions; + } + + /// + /// Adds the services. + /// + /// Services. + public void AddServices(IServiceCollection services) + { + services.AddOptions(); + + services.AddOptions() + .Configure(_confluentKafkaBusOptions); + + + //var options = services.BuildServiceProvider() + // .GetRequiredService>() + // .Value; + + services.AddSingleton(); + + } + } +} diff --git a/bus/EasyCaching.Bus.ConfluentKafka/Configurations/EasyCachingOptionsExtensions.cs b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/EasyCachingOptionsExtensions.cs new file mode 100644 index 00000000..4fd1cb98 --- /dev/null +++ b/bus/EasyCaching.Bus.ConfluentKafka/Configurations/EasyCachingOptionsExtensions.cs @@ -0,0 +1,81 @@ +namespace Microsoft.Extensions.DependencyInjection +{ + using Confluent.Kafka; + using EasyCaching.Bus.ConfluentKafka; + using EasyCaching.Core; + using EasyCaching.Core.Configurations; + using Microsoft.Extensions.Configuration; + using System; + + /// + /// EasyCaching options extensions. + /// + public static class EasyCachingOptionsExtensions + { + /// + /// Withs the ConfluentKafka bus (specify the config via hard code). + /// + /// + /// + /// + public static EasyCachingOptions WithConfluentKafkaBus( + this EasyCachingOptions options + , Action configure + ) + { + ArgumentCheck.NotNull(configure, nameof(configure)); + //option convert + ConfluentKafkaBusOptions kafkaOptions = new ConfluentKafkaBusOptions(); + configure.Invoke(kafkaOptions); + void kafkaBusConfigure(ConfluentKafkaBusOptions x) + { + x.BootstrapServers = kafkaOptions.BootstrapServers; + x.ProducerConfig = kafkaOptions.ProducerConfig ?? new ProducerConfig(); + x.ConsumerConfig = kafkaOptions.ConsumerConfig ?? new ConsumerConfig(); + //address + x.ProducerConfig.BootstrapServers = x.ProducerConfig.BootstrapServers ?? kafkaOptions.BootstrapServers; + x.ConsumerConfig.BootstrapServers = x.ConsumerConfig.BootstrapServers ?? kafkaOptions.BootstrapServers; + //consumer groupId + x.ConsumerConfig.GroupId = x.ConsumerConfig.GroupId ?? kafkaOptions.GroupId; + x.ConsumerConfig.AutoOffsetReset = kafkaOptions.ConsumerConfig.AutoOffsetReset ?? AutoOffsetReset.Latest; + } + + options.RegisterExtension(new ConfluentKafkaOptionsExtension(kafkaBusConfigure)); + return options; + } + + /// + /// Withs the ConfluentKafka bus (read config from configuration file). + /// + /// + /// + /// The section name in the configuration file. + /// + public static EasyCachingOptions WithConfluentKafkaBus( + this EasyCachingOptions options + , IConfiguration configuration + , string sectionName = EasyCachingConstValue.KafkaBusSection + ) + { + var dbConfig = configuration.GetSection(sectionName); + var kafkaOptions = new ConfluentKafkaBusOptions(); + dbConfig.Bind(kafkaOptions); + + void configure(ConfluentKafkaBusOptions x) + { + x.BootstrapServers = kafkaOptions.BootstrapServers; + x.ProducerConfig = kafkaOptions.ProducerConfig ?? new ProducerConfig(); + x.ConsumerConfig = kafkaOptions.ConsumerConfig ?? new ConsumerConfig(); + //address + x.ProducerConfig.BootstrapServers = x.ProducerConfig.BootstrapServers ?? kafkaOptions.BootstrapServers; + x.ConsumerConfig.BootstrapServers = x.ConsumerConfig.BootstrapServers ?? kafkaOptions.BootstrapServers; + //consumer groupId + x.ConsumerConfig.GroupId = x.ConsumerConfig.GroupId ?? kafkaOptions.GroupId; + x.ConsumerConfig.AutoOffsetReset = kafkaOptions.ConsumerConfig.AutoOffsetReset ?? AutoOffsetReset.Latest; + } + + options.RegisterExtension(new ConfluentKafkaOptionsExtension(configure)); + return options; + } + } +} diff --git a/bus/EasyCaching.Bus.ConfluentKafka/DefaultConfluentKafkaBus.cs b/bus/EasyCaching.Bus.ConfluentKafka/DefaultConfluentKafkaBus.cs new file mode 100644 index 00000000..296fd5b2 --- /dev/null +++ b/bus/EasyCaching.Bus.ConfluentKafka/DefaultConfluentKafkaBus.cs @@ -0,0 +1,149 @@ +namespace EasyCaching.Bus.ConfluentKafka +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Confluent.Kafka; + using EasyCaching.Core; + using EasyCaching.Core.Bus; + using EasyCaching.Core.Serialization; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using Microsoft.Extensions.Options; + + public class DefaultConfluentKafkaBus : EasyCachingAbstractBus + { + + + /// + /// The kafka Bus options. + /// + private readonly ConfluentKafkaBusOptions _kafkaBusOptions; + + /// + /// The serializer. + /// + private readonly IEasyCachingSerializer _serializer; + + /// + /// kafka producer object + /// + + private readonly IProducer _producer; + + + /// + /// log + /// + + private readonly ILogger _logger = NullLogger.Instance; + + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public DefaultConfluentKafkaBus( + IOptionsMonitor kafkaBusOptions + , IEasyCachingSerializer serializer) + { + this.BusName = "easycachingbus"; + this._kafkaBusOptions = kafkaBusOptions.CurrentValue; + + this._producer = new ProducerBuilder(this._kafkaBusOptions.ProducerConfig).Build(); + + this._serializer = serializer; + } + + /// + /// Publish the specified topic and message. + /// + /// Topic. + /// Message. + public override void BasePublish(string topic, EasyCachingMessage message) + { + var msg = _serializer.Serialize(message); + + _producer.Produce(topic, new Message { Value = msg }); + } + + /// + /// Publishs the async. + /// + /// The async. + /// Topic. + /// Message. + /// Cancellation token. + public override async Task BasePublishAsync(string topic, EasyCachingMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + var msg = _serializer.Serialize(message); + + await _producer.ProduceAsync(topic, new Message { Value = msg }); + } + + /// + /// Subscribe the specified topic and action. + /// + /// Topic. + /// Action. + public override void BaseSubscribe(string topic, Action action) + { + Task.Factory.StartNew(() => + { + for (int i = 0; i < this._kafkaBusOptions.ConsumerCount; i++) + { + using (var consumer = new ConsumerBuilder(this._kafkaBusOptions.ConsumerConfig).Build()) + { + consumer.Subscribe(topic); + try + { + while (true) + { + try + { + var cr = consumer.Consume(); + if (cr.IsPartitionEOF + || cr.Message == null + || cr.Message.Value.Length == 0) + { + continue; + } + OnMessage(cr.Message.Value); + } + catch (ConsumeException ex) + { + _logger.LogError(ex, "Consumer {0} error of reason {1}.", topic, ex.Error.Reason); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Consumer {0} error.", topic); + } + } + } + catch (OperationCanceledException ex) + { + _logger.LogWarning(ex, "Consumer {0} cancel.", topic); + consumer.Close(); + } + } + } + }, TaskCreationOptions.LongRunning); + } + + /// + /// Ons the message. + /// + /// Body. + private void OnMessage(byte[] body) + { + var message = _serializer.Deserialize(body); + + BaseOnMessage(message); + } + } +} diff --git a/bus/EasyCaching.Bus.ConfluentKafka/EasyCaching.Bus.ConfluentKafka.csproj b/bus/EasyCaching.Bus.ConfluentKafka/EasyCaching.Bus.ConfluentKafka.csproj new file mode 100644 index 00000000..52779cf2 --- /dev/null +++ b/bus/EasyCaching.Bus.ConfluentKafka/EasyCaching.Bus.ConfluentKafka.csproj @@ -0,0 +1,42 @@ + + + + + netstandard2.0;net6.0 + ncc;Catcher Wong + ncc;Catcher Wong + $(EasyCachingKafkaBusPackageVersion) + + + A simple caching bus(message bus) based on Kafka. + + Bus,Hybrid,Kafka,Caching,Cache + https://github.com/dotnetcore/EasyCaching + LICENSE + https://github.com/dotnetcore/EasyCaching + https://github.com/dotnetcore/EasyCaching + nuget-icon.png + + $(EasyCachingKafkaBusPackageNotes) + + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + diff --git a/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptionsExtension.cs b/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptionsExtension.cs index c77edd42..8891ec70 100644 --- a/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptionsExtension.cs +++ b/bus/EasyCaching.Bus.RabbitMQ/Configurations/RabbitMQBusOptionsExtension.cs @@ -37,7 +37,6 @@ public void AddServices(IServiceCollection services) services.AddOptions(); services.Configure(configure); - services.TryAddSingleton(); services.AddSingleton, ModelPooledObjectPolicy>(); services.AddSingleton(); } diff --git a/bus/EasyCaching.Bus.Redis/Configurations/RedisBusOptionsExtension.cs b/bus/EasyCaching.Bus.Redis/Configurations/RedisBusOptionsExtension.cs index fe06b869..6e18172b 100644 --- a/bus/EasyCaching.Bus.Redis/Configurations/RedisBusOptionsExtension.cs +++ b/bus/EasyCaching.Bus.Redis/Configurations/RedisBusOptionsExtension.cs @@ -44,7 +44,6 @@ public void AddServices(IServiceCollection services) services.AddOptions(); services.Configure(_name, configure); - services.TryAddSingleton(); services.AddSingleton(x => { var optionsMon = x.GetRequiredService>(); diff --git a/bus/EasyCaching.Bus.Zookeeper/Configurations/EasyCachingOptionsExtensions.cs b/bus/EasyCaching.Bus.Zookeeper/Configurations/EasyCachingOptionsExtensions.cs new file mode 100644 index 00000000..80d7452a --- /dev/null +++ b/bus/EasyCaching.Bus.Zookeeper/Configurations/EasyCachingOptionsExtensions.cs @@ -0,0 +1,64 @@ +namespace Microsoft.Extensions.DependencyInjection +{ + using EasyCaching.Bus.Zookeeper; + using EasyCaching.Core; + using EasyCaching.Core.Configurations; + using Microsoft.Extensions.Configuration; + using System; + + /// + /// EasyCaching options extensions. + /// + public static class EasyCachingOptionsExtensions + { + /// + /// Withs the Zookeeper bus (specify the config via hard code). + /// + /// + /// + /// + public static EasyCachingOptions WithZookeeeperBus( + this EasyCachingOptions options + , Action configure + ) + { + ArgumentCheck.NotNull(configure, nameof(configure)); + options.RegisterExtension(new ZookeeperOptionsExtension(configure)); + return options; + } + + /// + /// Withs the zookeeper bus (read config from configuration file). + /// + /// + /// + /// The section name in the configuration file. + /// + public static EasyCachingOptions WithConfluentKafkaBus( + this EasyCachingOptions options + , IConfiguration configuration + , string sectionName = EasyCachingConstValue.ZookeeperBusSection + ) + { + var dbConfig = configuration.GetSection(sectionName); + var zkOptions = new ZkBusOptions(); + dbConfig.Bind(zkOptions); + + void configure(ZkBusOptions x) + { + x.ConnectionString = zkOptions.ConnectionString; + x.SessionTimeout = zkOptions.SessionTimeout; + x.OperatingTimeout = zkOptions.OperatingTimeout; + x.ConnectionTimeout = zkOptions.ConnectionTimeout; + x.Digest = zkOptions.Digest; + x.BaseRoutePath = zkOptions.BaseRoutePath; + x.ReadOnly = zkOptions.ReadOnly; + x.BaseRoutePath = zkOptions.BaseRoutePath; + x.LogToFile = zkOptions.LogToFile; + } + + options.RegisterExtension(new ZookeeperOptionsExtension(configure)); + return options; + } + } +} diff --git a/bus/EasyCaching.Bus.Zookeeper/Configurations/ZkBusOptions.cs b/bus/EasyCaching.Bus.Zookeeper/Configurations/ZkBusOptions.cs new file mode 100644 index 00000000..5996a947 --- /dev/null +++ b/bus/EasyCaching.Bus.Zookeeper/Configurations/ZkBusOptions.cs @@ -0,0 +1,89 @@ +using System; + +namespace EasyCaching.Bus.Zookeeper +{ + public class ZkBusOptions + { + public ZkBusOptions() + { + this.ConnectionTimeout = 50000;//Milliseconds + this.OperatingTimeout = 10000; + this.SessionTimeout = 50000; + } + + /// + /// create ZooKeeper client + /// + /// + /// + public ZkBusOptions(string connectionString) + { + if (string.IsNullOrEmpty(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); + + ConnectionString = connectionString; + } + + /// + /// create ZooKeeper client + /// + /// + /// + /// + /// + /// + public ZkBusOptions(string connectionString + , int connectionTimeout + , int operatingTimeout + , int sessionTimeout) + { + if (string.IsNullOrEmpty(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); + + ConnectionString = connectionString; + this.ConnectionTimeout = connectionTimeout; + this.SessionTimeout = sessionTimeout; + this.OperatingTimeout = operatingTimeout; + } + + /// + /// connect string + /// + public string ConnectionString { get; set; } + + /// + /// readonly + /// + public bool ReadOnly { get; set; } = false; + + /// + /// point user to access + /// + public string Digest { get; set; } + + /// + /// log to file options + /// + public bool LogToFile { get; set; } = false; + + /// + /// base root path + /// + public string BaseRoutePath { get; set; } = "easyCacheBus"; + + /// + /// wait zooKeeper connect time with Milliseconds + /// + public int ConnectionTimeout { get; set; } + + /// + /// execute zooKeeper handler retry waittime with Milliseconds + /// + public int OperatingTimeout { get; set; } + + /// + /// zookeeper session timeout with Milliseconds + /// + public int SessionTimeout { get; set; } + } +} \ No newline at end of file diff --git a/bus/EasyCaching.Bus.Zookeeper/Configurations/ZookeeperOptionsExtension.cs b/bus/EasyCaching.Bus.Zookeeper/Configurations/ZookeeperOptionsExtension.cs new file mode 100644 index 00000000..901ff238 --- /dev/null +++ b/bus/EasyCaching.Bus.Zookeeper/Configurations/ZookeeperOptionsExtension.cs @@ -0,0 +1,36 @@ +namespace EasyCaching.Bus.Zookeeper +{ + using EasyCaching.Core.Bus; + using EasyCaching.Core.Configurations; + using EasyCaching.Core.Serialization; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using System; + + /// + /// Zookeeper options extension. + /// + internal sealed class ZookeeperOptionsExtension : IEasyCachingOptionsExtension + { + private readonly Action _zkBusOptions; + + public ZookeeperOptionsExtension(Action zkBusOptions) + { + this._zkBusOptions = zkBusOptions; + } + + /// + /// Adds the services. + /// + /// Services. + public void AddServices(IServiceCollection services) + { + services.AddOptions(); + + services.AddOptions() + .Configure(_zkBusOptions); + + services.AddSingleton(); + } + } +} \ No newline at end of file diff --git a/bus/EasyCaching.Bus.Zookeeper/DefaultZookeeperBus.cs b/bus/EasyCaching.Bus.Zookeeper/DefaultZookeeperBus.cs new file mode 100644 index 00000000..c507d008 --- /dev/null +++ b/bus/EasyCaching.Bus.Zookeeper/DefaultZookeeperBus.cs @@ -0,0 +1,376 @@ +namespace EasyCaching.Bus.Zookeeper +{ + using EasyCaching.Core; + using EasyCaching.Core.Bus; + using EasyCaching.Core.Serialization; + using Microsoft.Extensions.Options; + using org.apache.zookeeper; + using org.apache.zookeeper.data; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + public class DefaultZookeeperBus : EasyCachingAbstractBus + { + /// + /// The zookeeper Bus options. + /// + private readonly ZkBusOptions _zkBusOptions; + + /// + /// The zookeeper Client + /// + private ZooKeeper _zkClient; + + /// + /// zookeeper data chane delegate event + /// + /// + /// + + public delegate Task NodeDataChangeHandler(WatchedEvent @event); + + /// + /// event + /// + private NodeDataChangeHandler _dataChangeHandler; + + /// + /// lock + /// + private readonly object _zkEventLock = new object(); + + /// + /// The serializer. + /// + private readonly IEasyCachingSerializer _serializer; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public DefaultZookeeperBus( + IOptionsMonitor zkBusOptions + , IEasyCachingSerializer serializer) + { + this.BusName = "easycachingbus"; + this._zkBusOptions = zkBusOptions.CurrentValue; + this._zkClient = CreateClient(zkBusOptions.CurrentValue, new ZkNodeDataWatch(this)); + + this._serializer = serializer; + } + + /// + /// Publish the specified topic and message. + /// + /// Topic. + /// Message. + public override void BasePublish(string topic, EasyCachingMessage message) + { + var msg = _serializer.Serialize(message); + var path = $"{topic}"; + Task.Run(async () => + { + if (!await PathExistsAsync(path, true)) + { + await CreateRecursiveAsync(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + } + await SetDataAsync(path, msg); + }).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Publishs the async. + /// + /// The async. + /// Topic. + /// Message. + /// Cancellation token. + public override async Task BasePublishAsync(string topic, EasyCachingMessage message, CancellationToken cancellationToken = default(CancellationToken)) + { + var msg = _serializer.Serialize(message); + var path = $"{topic}"; + + if (!await PathExistsAsync(path, true)) + { + await CreateRecursiveAsync(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + } + await SetDataAsync(path, msg); + } + + /// + /// Subscribe the specified topic and action. + /// + /// Topic. + /// Action. + public override void BaseSubscribe(string topic, Action action) + { + var path = $"{topic}"; + Task.Factory.StartNew(async () => + { + await SubscribeDataChangeAsync(path, SubscribeDataChange); + }, TaskCreationOptions.LongRunning); + } + + /// + /// Ons the message. + /// + /// Body. + private void OnMessage(byte[] body) + { + var message = _serializer.Deserialize(body); + BaseOnMessage(message); + } + + /// + /// create zk client + /// + /// + /// + /// + /// + private ZooKeeper CreateClient(ZkBusOptions options, Watcher watcher) + { + ZooKeeper.LogToFile = options.LogToFile; + var zk = new ZooKeeper(options.ConnectionString, options.SessionTimeout, watcher); + if (!string.IsNullOrEmpty(options.Digest)) + { + zk.addAuthInfo("digest", Encoding.UTF8.GetBytes(options.Digest)); + } + var operationStartTime = DateTime.Now; + while (true) + { + if (zk.getState() == ZooKeeper.States.CONNECTING) + { + Thread.Sleep(100); + } + else if (zk.getState() == ZooKeeper.States.CONNECTED + || zk.getState() == ZooKeeper.States.CONNECTEDREADONLY) + { + return zk; + } + if (DateTime.Now - operationStartTime > TimeSpan.FromMilliseconds(options.OperatingTimeout)) + { + throw new TimeoutException( + $"connect cannot be retried because of retry timeout ({options.OperatingTimeout}Milliseconds)"); + } + } + } + + /// + /// subscribe data change + /// + /// + /// + private async Task SubscribeDataChange(WatchedEvent @event) + { + var state = @event.getState(); + if (state == Watcher.Event.KeeperState.Expired) + { + await ReZkConnect(); + } + + var eventType = @event.get_Type(); + byte[] nodeData = await GetDataAsync(@event.getPath()); + + switch (eventType) + { + case Watcher.Event.EventType.NodeCreated: + break; + + case Watcher.Event.EventType.NodeDeleted: + case Watcher.Event.EventType.NodeDataChanged: + if (!nodeData.Any()) + { + return; + } + + //hander business logical + OnMessage(nodeData); + break; + } + await Task.CompletedTask; + } + + /// + /// reconnnect zk + /// + /// + private async Task ReZkConnect() + { + if (!Monitor.TryEnter(_zkEventLock, _zkBusOptions.ConnectionTimeout)) + return; + try + { + if (_zkClient != null) + { + try + { + await _zkClient.closeAsync(); + } + catch + { + // ignored + } + } + + _zkClient = CreateClient(_zkBusOptions, new ZkNodeDataWatch(this)); + } + finally + { + Monitor.Exit(_zkEventLock); + } + } + + /// + /// subscribe data change + /// + /// + /// + /// + private async Task SubscribeDataChangeAsync(string path, NodeDataChangeHandler listener) + { + _dataChangeHandler += listener; + await PathExistsAsync(path, true); + } + + /// + /// pathExists + /// + /// + /// + /// + private async Task PathExistsAsync(string path, bool watch = false) + { + path = GetZooKeeperPath(path); + var state = await _zkClient.existsAsync(path, watch); + return state != null; + } + + /// + /// set node data + /// + /// + /// + /// + /// node stat + public async Task SetDataAsync(string path, byte[] data, int version = -1) + { + path = GetZooKeeperPath(path); + var stat = await _zkClient.setDataAsync(path, data, version); + return stat; + } + + /// + /// get data + /// + /// + /// + /// + public async Task GetDataAsync(string path, bool pathCv = false) + { + if (pathCv) + { + path = GetZooKeeperPath(path); + } + var data = await _zkClient.getDataAsync(path); + return data?.Data; + } + + /// + /// recurive create + /// + /// + /// + /// + /// + /// + private async Task CreateRecursiveAsync(string path, byte[] data, List acls, CreateMode createMode) + { + path = GetZooKeeperPath(path); + var paths = path.Trim('/').Split('/'); + var cur = ""; + foreach (var item in paths) + { + if (string.IsNullOrEmpty(item)) + { + continue; + } + cur += $"/{item}"; + var existStat = await _zkClient.existsAsync(cur, null); + if (existStat != null) + { + continue; + } + + if (cur.Equals(path)) + { + await _zkClient.createAsync(cur, data, acls, createMode); + } + else + { + await _zkClient.createAsync(cur, null, acls, createMode); + } + } + return await Task.FromResult(true); + } + + /// + /// + /// + /// + /// + private string GetZooKeeperPath(string path) + { + var basePath = _zkBusOptions.BaseRoutePath ?? "/"; + + if (!basePath.StartsWith("/")) + basePath = basePath.Insert(0, "/"); + + basePath = basePath.TrimEnd('/'); + + if (!path.StartsWith("/")) + path = path.Insert(0, "/"); + + path = $"{basePath}{path.TrimEnd('/')}"; + return string.IsNullOrEmpty(path) ? "/" : path; + } + + /// + /// watch zkNode data Change + /// + private class ZkNodeDataWatch : Watcher + { + private readonly DefaultZookeeperBus _defaultZookeeperBus; + + public ZkNodeDataWatch(DefaultZookeeperBus defaultZookeeperBus) + { + _defaultZookeeperBus = defaultZookeeperBus; + } + + public override async Task process(WatchedEvent watchedEvent) + { + var path = watchedEvent.getPath(); + if (path != null) + { + var eventType = watchedEvent.get_Type(); + var dataChanged = new[] + { + Watcher.Event.EventType.NodeCreated, + Watcher.Event.EventType.NodeDataChanged, + Watcher.Event.EventType.NodeDeleted + }.Contains(eventType); + + if (dataChanged) + { + await _defaultZookeeperBus._dataChangeHandler(watchedEvent); + } + } + } + } + } +} \ No newline at end of file diff --git a/bus/EasyCaching.Bus.Zookeeper/EasyCaching.Bus.Zookeeper.csproj b/bus/EasyCaching.Bus.Zookeeper/EasyCaching.Bus.Zookeeper.csproj new file mode 100644 index 00000000..5d754db8 --- /dev/null +++ b/bus/EasyCaching.Bus.Zookeeper/EasyCaching.Bus.Zookeeper.csproj @@ -0,0 +1,43 @@ + + + + + + netstandard2.0;net6.0 + ncc;Catcher Wong + ncc;Catcher Wong + $(EasyCachingZookeeperBusPackageVersion) + + + A simple caching bus(message bus) based on Zookeeper. + + Bus,Hybrid,Zookeeper,Caching,Cache + https://github.com/dotnetcore/EasyCaching + LICENSE + https://github.com/dotnetcore/EasyCaching + https://github.com/dotnetcore/EasyCaching + nuget-icon.png + + $(EasyCachingZookeeperBusPackageNotes) + + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + diff --git a/sample/EasyCaching.Demo.Providers/Controllers/ValuesBusController.cs b/sample/EasyCaching.Demo.Providers/Controllers/ValuesBusController.cs new file mode 100644 index 00000000..abb76ab8 --- /dev/null +++ b/sample/EasyCaching.Demo.Providers/Controllers/ValuesBusController.cs @@ -0,0 +1,46 @@ +namespace EasyCaching.Demo.Providers.Controllers +{ + using EasyCaching.Core; + using Microsoft.AspNetCore.Mvc; + using System; + using System.Threading.Tasks; + + [Route("api/[controller]")] + public class ValuesBusController : Controller + { + //2. Hybird Cache + private readonly IHybridCachingProvider _provider; + private readonly IEasyCachingProviderFactory _factory; + + public ValuesBusController(IHybridCachingProvider provider, IEasyCachingProviderFactory factory) + { + this._provider = provider; + _factory = factory; + } + + // GET api/values + [HttpGet] + [Route("get2")] + public async Task Get2() + { + var rd = new Random(1000); + for (int i = 0; i < 5; i++) + { + var val = rd.Next().ToString(); + await _provider.SetAsync($"demo{i}", val, TimeSpan.FromSeconds(5000)); + var provider = _factory.GetCachingProvider("cus"); + var v1 = provider.Get($"demow{i}"); + //Console.WriteLine($"{i}-->{v1}"); + + await _provider.SetAsync($"demow{i}", $"changeda-{val}", TimeSpan.FromSeconds(5000)); + + //var v2 = provider.Get($"demo{i}"); + //Console.WriteLine($"after--{i}-->{v2}"); + //Console.WriteLine("------------------"); + } + return $"hybrid"; + } + + + } +} diff --git a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj index f9afa091..0fb6da6f 100644 --- a/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj +++ b/sample/EasyCaching.Demo.Providers/EasyCaching.Demo.Providers.csproj @@ -5,6 +5,8 @@ + + diff --git a/sample/EasyCaching.Demo.Providers/Program.cs b/sample/EasyCaching.Demo.Providers/Program.cs index 81dd13f5..dbf64033 100644 --- a/sample/EasyCaching.Demo.Providers/Program.cs +++ b/sample/EasyCaching.Demo.Providers/Program.cs @@ -1,7 +1,9 @@ namespace EasyCaching.Demo.Providers { using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; + using System.IO; public class Program { @@ -12,6 +14,15 @@ public static void Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hosting, config) => + { + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{hosting.HostingEnvironment.EnvironmentName}.json", optional: true, + true); + + config.AddEnvironmentVariables(); + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/sample/EasyCaching.Demo.Providers/Startup.cs b/sample/EasyCaching.Demo.Providers/Startup.cs index 84160cc2..9f9e30f5 100644 --- a/sample/EasyCaching.Demo.Providers/Startup.cs +++ b/sample/EasyCaching.Demo.Providers/Startup.cs @@ -1,87 +1,87 @@ -namespace EasyCaching.Demo.Providers +namespace EasyCaching.Demo.Providers { using EasyCaching.Core.Configurations; using EasyCaching.SQLite; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - //new configuration + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + //new configuration services.AddEasyCaching(option => - { - //use memory cache - option.UseInMemory("default"); - - //use memory cache - option.UseInMemory("cus"); - - //use redis cache + { + //use memory cache + option.UseInMemory("default"); + + //use memory cache + option.UseInMemory("cus"); + + //use redis cache option.UseRedis(config => - { - config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); - config.DBConfig.SyncTimeout = 10000; - config.DBConfig.AsyncTimeout = 10000; - config.SerializerName = "mymsgpack"; - }, "redis1") - .WithMessagePack("mymsgpack")//with messagepack serialization - ; - - //use redis cache + { + config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); + config.DBConfig.SyncTimeout = 10000; + config.DBConfig.AsyncTimeout = 10000; + config.SerializerName = "mymsgpack"; + }, "redis1") + .WithMessagePack("mymsgpack")//with messagepack serialization + ; + + //use redis cache option.UseRedis(config => - { - config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6380)); - }, "redis2"); - - //use sqlite cache - option.UseSQLite(config => - { - config.DBConfig = new SQLiteDBOptions { FileName = "my.db" }; - }); - - //use memcached cached + { + config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6380)); + }, "redis2"); + + //use sqlite cache + option.UseSQLite(config => + { + config.DBConfig = new SQLiteDBOptions { FileName = "my.db" }; + }); + + //use memcached cached option.UseMemcached(config => - { - config.DBConfig.AddServer("127.0.0.1", 11211); - }); - - option.UseMemcached(Configuration); + { + config.DBConfig.AddServer("127.0.0.1", 11211); + }); + + option.UseMemcached(Configuration); }); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - //loggerFactory.AddConsole(Configuration.GetSection("Logging")); - - // Important step for using Memcached Cache or SQLite Cache - //app.UseEasyCaching(); - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + //loggerFactory.AddConsole(Configuration.GetSection("Logging")); + + // Important step for using Memcached Cache or SQLite Cache + //app.UseEasyCaching(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } } diff --git a/sample/EasyCaching.Demo.Providers/appsettings.json b/sample/EasyCaching.Demo.Providers/appsettings.json index 8f4acb34..85b08a94 100644 --- a/sample/EasyCaching.Demo.Providers/appsettings.json +++ b/sample/EasyCaching.Demo.Providers/appsettings.json @@ -12,59 +12,70 @@ } } }, - "easycaching": { - "inmemory": { - "CachingProviderType": 1, - "MaxRdSecond": 120, - "Order": 2, - }, - "sqlite": { - "CachingProviderType": 3, - "MaxRdSecond": 120, - "Order": 2, - "dbconfig": { - "FileName": "my.db" - } - }, - "redis": { - "CachingProviderType": 2, - "MaxRdSecond": 120, - "Order": 2, - "dbconfig": { - "Password": null, - "IsSsl": false, - "SslHost": null, - "ConnectionTimeout": 5000, - "AllowAdmin": true, - "Endpoints": [ - { - "Host": "localhost", - "Port": 6739 - } - ], - "Database": 0 - } - }, - "memcached": { - "CachingProviderType": 4, - "MaxRdSecond": 120, - "Order": 2, - "dbconfig": { - "Servers": [ - { - "Address": "127.0.0.1", - "Port": 11211 - } - ], - "socketPool": { - "minPoolSize": "5", - "maxPoolSize": "25", - "connectionTimeout": "00:00:15", - "receiveTimeout": "00:00:15", - "deadTimeout": "00:00:15", - "queueTimeout": "00:00:00.150" - } - } + "easycaching": { + "inmemory": { + "CachingProviderType": 1, + "MaxRdSecond": 120, + "Order": 2 + }, + "sqlite": { + "CachingProviderType": 3, + "MaxRdSecond": 120, + "Order": 2, + "dbconfig": { + "FileName": "my.db" + } + }, + "redis": { + "CachingProviderType": 2, + "MaxRdSecond": 120, + "Order": 2, + "dbconfig": { + "Password": null, + "IsSsl": false, + "SslHost": null, + "ConnectionTimeout": 5000, + "AllowAdmin": true, + "Endpoints": [ + { + "Host": "127.0.0.1", + "Port": 6739 + } + ], + "Database": 0 + } + }, + "memcached": { + "CachingProviderType": 4, + "MaxRdSecond": 120, + "Order": 2, + "dbconfig": { + "Servers": [ + { + "Address": "127.0.0.1", + "Port": 11211 + } + ], + "socketPool": { + "minPoolSize": "5", + "maxPoolSize": "25", + "connectionTimeout": "00:00:15", + "receiveTimeout": "00:00:15", + "deadTimeout": "00:00:15", + "queueTimeout": "00:00:00.150" } + } + }, + "kafkabus": { + // "BootstrapServers": "127.0.0.1:9093", + "ProducerConfig": { + "BootstrapServers": "127.0.0.1:9093" + }, + "ConsumerConfig": { + "BootstrapServers": "127.0.0.1:9093", + "GroupId": "MyGroupId" + }, + "ConsumerCount":2 } + } } diff --git a/src/EasyCaching.CSRedis/Configurations/RedisOptionsExtension.cs b/src/EasyCaching.CSRedis/Configurations/RedisOptionsExtension.cs index 4b1b9230..0f2deaa1 100755 --- a/src/EasyCaching.CSRedis/Configurations/RedisOptionsExtension.cs +++ b/src/EasyCaching.CSRedis/Configurations/RedisOptionsExtension.cs @@ -48,8 +48,7 @@ public void AddServices(IServiceCollection services) { services.AddOptions(); - services.TryAddSingleton(); - + services.Configure(_name, _configure); services.TryAddSingleton(); diff --git a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs index 501d0778..2135fb0f 100644 --- a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs +++ b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.Async.cs @@ -299,6 +299,33 @@ public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationTo await Task.WhenAll(tasks); } + /// + /// Removes the by pattern async. + /// + /// The by pattern async. + /// Pattern. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + pattern = this.HandleKeyPattern(pattern); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPatternAsync : pattern = {pattern}"); + + var redisKeys = this.SearchRedisKeys(pattern); + + var tasks = new List>(); + + foreach (var item in redisKeys) + { + tasks.Add(_cache.DelAsync(item)); + } + + await Task.WhenAll(tasks); + } + /// /// Sets all async. /// diff --git a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.cs b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.cs index 5a3a8790..f335eb7e 100644 --- a/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.cs +++ b/src/EasyCaching.CSRedis/DefaultCSRedisCachingProvider.cs @@ -275,6 +275,22 @@ private string HandlePrefix(string prefix) return prefix; } + + /// + /// Handles the pattern of CacheKey. + /// + /// Pattern of CacheKey. + private string HandleKeyPattern(string pattern) + { + // Forbid + if (pattern.Equals("*")) + throw new ArgumentException("the pattern should not equal to *"); + + if (!string.IsNullOrWhiteSpace(_cache.Nodes?.Values?.FirstOrDefault()?.Prefix)) + pattern = _cache.Nodes?.Values?.FirstOrDefault()?.Prefix + pattern; + + return pattern; + } /// /// Searchs the redis keys. @@ -401,6 +417,27 @@ public override void BaseRemoveByPrefix(string prefix) } } + /// + /// Remove cached value by pattern + /// + /// The pattern of cache key + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + pattern = this.HandleKeyPattern(pattern); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPattern : pattern = {pattern}"); + + var redisKeys = this.SearchRedisKeys(pattern); + + foreach (var item in redisKeys) + { + _cache.Del(item); + } + } + /// /// Set the specified cacheKey, cacheValue and expiration. /// diff --git a/src/EasyCaching.Core/Bus/EasyCachingMessage.cs b/src/EasyCaching.Core/Bus/EasyCachingMessage.cs index 09bf6e1a..ab728a16 100644 --- a/src/EasyCaching.Core/Bus/EasyCachingMessage.cs +++ b/src/EasyCaching.Core/Bus/EasyCachingMessage.cs @@ -25,5 +25,11 @@ public class EasyCachingMessage /// /// true if is prefix; otherwise, false. public bool IsPrefix { get; set; } + + /// + /// Gets or sets a value indicating whether this is pattern. + /// + /// true if is pattern; otherwise, false. + public bool IsPattern { get; set; } } } diff --git a/src/EasyCaching.Core/EasyCachingAbstractProvider.cs b/src/EasyCaching.Core/EasyCachingAbstractProvider.cs index 6f06028d..e7e348af 100644 --- a/src/EasyCaching.Core/EasyCachingAbstractProvider.cs +++ b/src/EasyCaching.Core/EasyCachingAbstractProvider.cs @@ -5,20 +5,20 @@ namespace EasyCaching.Core using System; using System.Collections.Generic; using System.Diagnostics; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using EasyCaching.Core.Configurations; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using EasyCaching.Core.Configurations; using EasyCaching.Core.Diagnostics; public abstract class EasyCachingAbstractProvider : IEasyCachingProvider { protected static readonly DiagnosticListener s_diagnosticListener = - new DiagnosticListener(EasyCachingDiagnosticListenerExtensions.DiagnosticListenerName); - - private readonly IDistributedLockFactory _lockFactory; - private readonly BaseProviderOptions _options; - + new DiagnosticListener(EasyCachingDiagnosticListenerExtensions.DiagnosticListenerName); + + private readonly IDistributedLockFactory _lockFactory; + private readonly BaseProviderOptions _options; + protected string ProviderName { get; set; } protected bool IsDistributedProvider { get; set; } protected int ProviderMaxRdSecond { get; set; } @@ -36,13 +36,13 @@ public abstract class EasyCachingAbstractProvider : IEasyCachingProvider protected EasyCachingAbstractProvider() { } - protected EasyCachingAbstractProvider(IDistributedLockFactory lockFactory, BaseProviderOptions options) - { - _lockFactory = lockFactory; - _options = options; - } - - public abstract object BaseGetDatabse(); + protected EasyCachingAbstractProvider(IDistributedLockFactory lockFactory, BaseProviderOptions options) + { + _lockFactory = lockFactory; + _options = options; + } + + public abstract object BaseGetDatabse(); public abstract bool BaseExists(string cacheKey); public abstract Task BaseExistsAsync(string cacheKey, CancellationToken cancellationToken = default); public abstract void BaseFlush(); @@ -64,6 +64,8 @@ protected EasyCachingAbstractProvider(IDistributedLockFactory lockFactory, BaseP public abstract Task BaseRemoveAsync(string cacheKey, CancellationToken cancellationToken = default); public abstract void BaseRemoveByPrefix(string prefix); public abstract Task BaseRemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default); + public abstract void BaseRemoveByPattern(string pattern); + public abstract Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default); public abstract void BaseSet(string cacheKey, T cacheValue, TimeSpan expiration); public abstract void BaseSetAll(IDictionary values, TimeSpan expiration); public abstract Task BaseSetAllAsync(IDictionary values, TimeSpan expiration, CancellationToken cancellationToken = default); @@ -185,30 +187,30 @@ public CacheValue Get(string cacheKey, Func dataRetriever, TimeSpan exp var operationId = s_diagnosticListener.WriteGetCacheBefore(new BeforeGetRequestEventData(CachingProviderType.ToString(), Name, nameof(Get), new[] { cacheKey }, expiration)); Exception e = null; try - { + { if (_lockFactory == null) return BaseGet(cacheKey, dataRetriever, expiration); var value = BaseGet(cacheKey); - if (value.HasValue) return value; - - using (var @lock = _lockFactory.CreateLock(Name, $"{cacheKey}_Lock")) - { + if (value.HasValue) return value; + + using (var @lock = _lockFactory.CreateLock(Name, $"{cacheKey}_Lock")) + { if (!@lock.Lock(_options.SleepMs)) throw new TimeoutException(); value = BaseGet(cacheKey); - if (value.HasValue) return value; - - var item = dataRetriever(); - if (item != null || _options.CacheNulls) - { - BaseSet(cacheKey, item, expiration); - - return new CacheValue(item, true); - } - else - { - return CacheValue.NoValue; - } + if (value.HasValue) return value; + + var item = dataRetriever(); + if (item != null || _options.CacheNulls) + { + BaseSet(cacheKey, item, expiration); + + return new CacheValue(item, true); + } + else + { + return CacheValue.NoValue; + } } } catch (Exception ex) @@ -316,36 +318,36 @@ public async Task> GetAsync(string cacheKey, Func> data if (_lockFactory == null) return await BaseGetAsync(cacheKey, dataRetriever, expiration, cancellationToken); var value = await BaseGetAsync(cacheKey); - if (value.HasValue) return value; - - var @lock = _lockFactory.CreateLock(Name, $"{cacheKey}_Lock"); - try - { + if (value.HasValue) return value; + + var @lock = _lockFactory.CreateLock(Name, $"{cacheKey}_Lock"); + try + { if (!await @lock.LockAsync(_options.SleepMs)) throw new TimeoutException(); value = await BaseGetAsync(cacheKey, cancellationToken); - if (value.HasValue) return value; - - var task = dataRetriever(); - if (!task.IsCompleted && - await Task.WhenAny(task, Task.Delay(_options.LockMs)) != task) - throw new TimeoutException(); - - var item = await task; - if (item != null || _options.CacheNulls) - { - await BaseSetAsync(cacheKey, item, expiration, cancellationToken); - - return new CacheValue(item, true); - } - else - { - return CacheValue.NoValue; - } - } - finally - { - await @lock.DisposeAsync(); + if (value.HasValue) return value; + + var task = dataRetriever(); + if (!task.IsCompleted && + await Task.WhenAny(task, Task.Delay(_options.LockMs)) != task) + throw new TimeoutException(); + + var item = await task; + if (item != null || _options.CacheNulls) + { + await BaseSetAsync(cacheKey, item, expiration, cancellationToken); + + return new CacheValue(item, true); + } + else + { + return CacheValue.NoValue; + } + } + finally + { + await @lock.DisposeAsync(); } } catch (Exception ex) @@ -473,8 +475,8 @@ public async Task>> GetByPrefixAsync(string public int GetCount(string prefix = "") { return BaseGetCount(prefix); - } - + } + public async Task GetCountAsync(string prefix = "", CancellationToken cancellationToken = default) { return await BaseGetCountAsync(prefix, cancellationToken); @@ -636,6 +638,62 @@ public async Task RemoveByPrefixAsync(string prefix, CancellationToken cancellat } } + public void RemoveByPattern(string pattern) + { + var operationId = s_diagnosticListener.WriteRemoveCacheBefore( + new BeforeRemoveRequestEventData(CachingProviderType.ToString(), Name, nameof(RemoveByPattern), + new[] { pattern })); + Exception e = null; + try + { + BaseRemoveByPattern(pattern); + } + catch (Exception ex) + { + e = ex; + throw; + } + finally + { + if (e != null) + { + s_diagnosticListener.WriteRemoveCacheError(operationId, e); + } + else + { + s_diagnosticListener.WriteRemoveCacheAfter(operationId); + } + } + } + + public async Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + var operationId = s_diagnosticListener.WriteRemoveCacheBefore( + new BeforeRemoveRequestEventData(CachingProviderType.ToString(), Name, nameof(RemoveByPatternAsync), + new[] { pattern })); + Exception e = null; + try + { + await BaseRemoveByPatternAsync(pattern, cancellationToken); + } + catch (Exception ex) + { + e = ex; + throw; + } + finally + { + if (e != null) + { + s_diagnosticListener.WriteRemoveCacheError(operationId, e); + } + else + { + s_diagnosticListener.WriteRemoveCacheAfter(operationId); + } + } + } + public void Set(string cacheKey, T cacheValue, TimeSpan expiration) { var operationId = s_diagnosticListener.WriteSetCacheBefore(new BeforeSetRequestEventData(CachingProviderType.ToString(), Name, nameof(Set), new Dictionary { { cacheKey, cacheValue } }, expiration)); @@ -804,7 +862,37 @@ public async Task GetExpirationAsync(string cacheKey, CancellationToke public ProviderInfo GetProviderInfo() { - return BaseGetProviderInfo(); + return BaseGetProviderInfo(); + } + + protected SearchKeyPattern ProcessSearchKeyPattern(string pattern) + { + var postfix = pattern.StartsWith("*"); + var prefix = pattern.EndsWith("*"); + + var contains = postfix && prefix; + + if (contains) + { + return SearchKeyPattern.Contains; + } + + if (postfix) + { + return SearchKeyPattern.Postfix; + } + + if (prefix) + { + return SearchKeyPattern.Prefix; + } + + return SearchKeyPattern.Exact; } + + protected string HandleSearchKeyPattern(string pattern) + { + return pattern.Replace("*", string.Empty); + } } } diff --git a/src/EasyCaching.Core/IEasyCachingProviderBase.cs b/src/EasyCaching.Core/IEasyCachingProviderBase.cs index 9b232e18..3c431f21 100644 --- a/src/EasyCaching.Core/IEasyCachingProviderBase.cs +++ b/src/EasyCaching.Core/IEasyCachingProviderBase.cs @@ -159,6 +159,19 @@ public interface IEasyCachingProviderBase /// Prefix of CacheKey. void RemoveByPrefix(string prefix); + /// + /// Removes cached items by a cache key pattern async. + /// + /// Pattern of CacheKeys. + /// + Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default); + + /// + /// Removes cached items by a cache key pattern. + /// + /// Pattern of CacheKeys. + void RemoveByPattern(string pattern); + /// /// Removes cached item by cachekey's prefix async. /// @@ -175,4 +188,4 @@ public interface IEasyCachingProviderBase /// Task GetAsync(string cacheKey, Type type, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs index 04fb045c..4e471456 100644 --- a/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs +++ b/src/EasyCaching.Core/Internal/EasyCachingConstValue.cs @@ -53,7 +53,18 @@ public class EasyCachingConstValue /// /// The rabbitMQ Bus section. /// - public const string RabbitMQBusSection = "easycaching:rabbitmqbus"; + public const string RabbitMQBusSection = "easycaching:rabbitmqbus"; + + /// + /// The kafka bus section. + /// + public const string KafkaBusSection = "easycaching:kafkabus"; + + /// + /// The zookeeper bus section. + /// + public const string ZookeeperBusSection = "easycaching:zookeeperbus"; + /// /// The default name of the in-memory. diff --git a/src/EasyCaching.Core/SearchKeyPattern.cs b/src/EasyCaching.Core/SearchKeyPattern.cs new file mode 100644 index 00000000..1d09ac6f --- /dev/null +++ b/src/EasyCaching.Core/SearchKeyPattern.cs @@ -0,0 +1,10 @@ +namespace EasyCaching.Core +{ + public enum SearchKeyPattern + { + Postfix, + Prefix, + Contains, + Exact + } +} diff --git a/src/EasyCaching.Core/Serialization/DefaultBinaryFormatterSerializer.cs b/src/EasyCaching.Core/Serialization/DefaultBinaryFormatterSerializer.cs deleted file mode 100644 index 937b01ba..00000000 --- a/src/EasyCaching.Core/Serialization/DefaultBinaryFormatterSerializer.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace EasyCaching.Core.Serialization -{ - using System; - using System.IO; - using System.Runtime.Serialization.Formatters.Binary; - - /// - /// Default binary formatter serializer. - /// - public class DefaultBinaryFormatterSerializer : IEasyCachingSerializer - { - /// - /// Gets the name. - /// - /// The name. - public string Name => EasyCachingConstValue.DefaultSerializerName; - - /// - /// Deserialize the specified bytes. - /// - /// The deserialize. - /// Bytes. - /// The 1st type parameter. - public T Deserialize(byte[] bytes) - { - if (bytes.Length == 0) - { - return (T)(object) null; - } - - using (var ms = new MemoryStream(bytes)) - { - return (T)(new BinaryFormatter().Deserialize(ms)); - } - } - - /// - /// Deserialize the specified bytes. - /// - /// The deserialize. - /// Bytes. - /// Type. - public object Deserialize(byte[] bytes, Type type) - { - if (bytes.Length == 0) - { - return null; - } - - using (var ms = new MemoryStream(bytes)) - { - return (new BinaryFormatter().Deserialize(ms)); - } - } - - /// - /// Deserializes the object. - /// - /// The object. - /// Value. - public object DeserializeObject(ArraySegment value) - { - using (var ms = new MemoryStream(value.Array, value.Offset, value.Count)) - { - return new BinaryFormatter().Deserialize(ms); - } - } - - /// - /// Serialize the specified value. - /// - /// The serialize. - /// Value. - /// The 1st type parameter. - public byte[] Serialize(T value) - { - if (value == null) - { - return Array.Empty(); - } - - using (var ms = new MemoryStream()) - { - new BinaryFormatter().Serialize(ms, value); - return ms.ToArray(); - } - } - - /// - /// Serializes the object. - /// - /// The object. - /// Object. - public ArraySegment SerializeObject(object obj) - { - if (obj == null) - { - return new ArraySegment(Array.Empty()); - } - - using (var ms = new MemoryStream()) - { - new BinaryFormatter().Serialize(ms, obj); - return new ArraySegment(ms.GetBuffer(), 0, (int)ms.Length); - } - } - } -} diff --git a/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs b/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs index 4e74545e..e0f15fa2 100644 --- a/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs +++ b/src/EasyCaching.Disk/DefaultDiskCachingProvider.Async.cs @@ -362,7 +362,29 @@ public override Task BaseRemoveByPrefixAsync(string prefix, CancellationToken ca return Task.CompletedTask; } - + + public override Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + var searchPattern = this.ProcessSearchKeyPattern(pattern); + var searchKey = this.HandleSearchKeyPattern(pattern); + + var list = _cacheKeysMap.Where(pair => FilterByPattern(pair.Key,searchKey, searchPattern)).Select(x => x.Key).ToList(); + + foreach (var item in list) + { + var path = BuildMd5Path(item); + + if (DeleteFileWithRetry(path)) + { + _cacheKeysMap.TryRemove(item, out _); + } + } + + return Task.CompletedTask; + } + public override async Task BaseSetAllAsync(IDictionary values, TimeSpan expiration, CancellationToken cancellationToken = default) { ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); diff --git a/src/EasyCaching.Disk/DefaultDiskCachingProvider.cs b/src/EasyCaching.Disk/DefaultDiskCachingProvider.cs index b85cfd30..d3541701 100644 --- a/src/EasyCaching.Disk/DefaultDiskCachingProvider.cs +++ b/src/EasyCaching.Disk/DefaultDiskCachingProvider.cs @@ -469,6 +469,26 @@ public override void BaseRemoveByPrefix(string prefix) } } + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + var searchPattern = this.ProcessSearchKeyPattern(pattern); + var searchKey = this.HandleSearchKeyPattern(pattern); + + var list = _cacheKeysMap.Where(pair => FilterByPattern(pair.Key,searchKey, searchPattern)).Select(x => x.Key).ToList(); + + foreach (var item in list) + { + var path = BuildMd5Path(item); + + if (DeleteFileWithRetry(path)) + { + _cacheKeysMap.TryRemove(item, out _); + } + } + } + public override void BaseSet(string cacheKey, T cacheValue, TimeSpan expiration) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); @@ -542,6 +562,23 @@ public override bool BaseTrySet(string cacheKey, T cacheValue, TimeSpan expir return true; } } + + private static bool FilterByPattern(string key, string searchKey, SearchKeyPattern searchKeyPattern) + { + switch (searchKeyPattern) + { + case SearchKeyPattern.Postfix: + return key.EndsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Prefix: + return key.StartsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Contains: + return key.Contains(searchKey); + case SearchKeyPattern.Exact: + return key.Equals(searchKey, StringComparison.Ordinal); + default: + throw new ArgumentOutOfRangeException(nameof(searchKeyPattern), searchKeyPattern, null); + } + } private (string path, string md5Name) GetFilePath(string key) { diff --git a/src/EasyCaching.HybridCache/HybridCachingProvider.cs b/src/EasyCaching.HybridCache/HybridCachingProvider.cs index 92f7ab79..cc6f830c 100644 --- a/src/EasyCaching.HybridCache/HybridCachingProvider.cs +++ b/src/EasyCaching.HybridCache/HybridCachingProvider.cs @@ -2,8 +2,8 @@ { using System; using System.Collections.Generic; - using System.Linq; - using System.Threading; + using System.Linq; + using System.Threading; using System.Threading.Tasks; using EasyCaching.Core; using EasyCaching.Core.Bus; @@ -41,7 +41,7 @@ public class HybridCachingProvider : IHybridCachingProvider /// /// The cache identifier. /// - private readonly string _cacheId; + private readonly string _cacheId; /// /// The name. /// @@ -52,10 +52,10 @@ public class HybridCachingProvider : IHybridCachingProvider private readonly RetryPolicy retryPolicy; private readonly AsyncRetryPolicy retryAsyncPolicy; private readonly FallbackPolicy fallbackPolicy; - private readonly AsyncFallbackPolicy fallbackAsyncPolicy; - - public string Name => _name; - + private readonly AsyncFallbackPolicy fallbackAsyncPolicy; + + public string Name => _name; + /// /// Initializes a new instance of the class. /// @@ -72,8 +72,8 @@ string name , ILoggerFactory loggerFactory = null ) { - ArgumentCheck.NotNull(factory, nameof(factory)); - + ArgumentCheck.NotNull(factory, nameof(factory)); + this._name = name; this._options = optionsAccs; @@ -127,6 +127,18 @@ private void OnMessage(EasyCachingMessage message) if (!string.IsNullOrWhiteSpace(message.Id) && message.Id.Equals(_cacheId, StringComparison.OrdinalIgnoreCase)) return; + // remove by pattern + if (message.IsPattern) + { + var pattern = message.CacheKeys.First(); + + _localCache.RemoveByPattern(pattern); + + LogMessage($"remove local cache that pattern is {pattern}"); + + return; + } + // remove by prefix if (message.IsPrefix) { @@ -610,12 +622,12 @@ public CacheValue Get(string cacheKey, Func dataRetriever, TimeSpan exp LogMessage($"get with data retriever from distributed provider error [{cacheKey}]", ex); } - if (result.HasValue) - { + if (result.HasValue) + { TimeSpan ts = GetExpiration(cacheKey); - _localCache.Set(cacheKey, result.Value, ts); - + _localCache.Set(cacheKey, result.Value, ts); + return result; } @@ -650,14 +662,14 @@ public async Task> GetAsync(string cacheKey, Func> data catch (Exception ex) { LogMessage($"get async with data retriever from distributed provider error [{cacheKey}]", ex); - } - - if (result.HasValue) - { + } + + if (result.HasValue) + { TimeSpan ts = await GetExpirationAsync(cacheKey, cancellationToken); - _localCache.Set(cacheKey, result.Value, ts); - + _localCache.Set(cacheKey, result.Value, ts); + return result; } @@ -711,6 +723,55 @@ public async Task RemoveByPrefixAsync(string prefix, CancellationToken cancellat // send message to bus in order to notify other clients. await _busAsyncWrap.ExecuteAsync(async (ct) => await _bus.PublishAsync(_options.TopicName, new EasyCachingMessage { Id = _cacheId, CacheKeys = new string[] { prefix }, IsPrefix = true }, ct), cancellationToken); } + + /// + /// Removes the by pattern async. + /// + /// The by pattern async. + /// Pattern. + /// CancellationToken + public async Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + try + { + await _distributedCache.RemoveByPatternAsync(pattern, cancellationToken); + } + catch (Exception ex) + { + LogMessage($"remove by pattern [{pattern}] error", ex); + } + + await _localCache.RemoveByPatternAsync(pattern); + + // send message to bus in order to notify other clients. + await _busAsyncWrap.ExecuteAsync(async (ct) => await _bus.PublishAsync(_options.TopicName, new EasyCachingMessage { Id = _cacheId, CacheKeys = new string[] { pattern }, IsPattern = true}, ct), cancellationToken); + } + + /// + /// Removes the by pattern. + /// + /// The by pattern. + /// Pattern. + public void RemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + try + { + _distributedCache.RemoveByPattern(pattern); + } + catch (Exception ex) + { + LogMessage($"remove by pattern [{pattern}] error", ex); + } + + _localCache.RemoveByPattern(pattern); + + // send message to bus + _busSyncWrap.Execute(() => _bus.Publish(_options.TopicName, new EasyCachingMessage { Id = _cacheId, CacheKeys = new string[] { pattern }, IsPattern = true})); + } /// /// Logs the message. @@ -732,43 +793,43 @@ private void LogMessage(string message, Exception ex = null) } } - private async Task GetExpirationAsync(string cacheKey, CancellationToken cancellationToken = default) - { - TimeSpan ts = TimeSpan.Zero; - - try - { - ts = await _distributedCache.GetExpirationAsync(cacheKey, cancellationToken); - } - catch - { - - } - - if (ts <= TimeSpan.Zero) - { - ts = TimeSpan.FromSeconds(_options.DefaultExpirationForTtlFailed); + private async Task GetExpirationAsync(string cacheKey, CancellationToken cancellationToken = default) + { + TimeSpan ts = TimeSpan.Zero; + + try + { + ts = await _distributedCache.GetExpirationAsync(cacheKey, cancellationToken); + } + catch + { + + } + + if (ts <= TimeSpan.Zero) + { + ts = TimeSpan.FromSeconds(_options.DefaultExpirationForTtlFailed); } return ts; } - private TimeSpan GetExpiration(string cacheKey) - { - TimeSpan ts = TimeSpan.Zero; - - try - { - ts = _distributedCache.GetExpiration(cacheKey); - } - catch - { - - } - - if (ts <= TimeSpan.Zero) - { - ts = TimeSpan.FromSeconds(_options.DefaultExpirationForTtlFailed); + private TimeSpan GetExpiration(string cacheKey) + { + TimeSpan ts = TimeSpan.Zero; + + try + { + ts = _distributedCache.GetExpiration(cacheKey); + } + catch + { + + } + + if (ts <= TimeSpan.Zero) + { + ts = TimeSpan.FromSeconds(_options.DefaultExpirationForTtlFailed); } return ts; diff --git a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs index cfa87be8..45ea96ab 100644 --- a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs +++ b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.Async.cs @@ -224,6 +224,25 @@ public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationTo _logger?.LogInformation($"RemoveByPrefixAsync : prefix = {prefix} , count = {count}"); } + /// + /// Removes cached items by pattern async. + /// + /// The by prefix async. + /// Pattern. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + var searchPattern = this.ProcessSearchKeyPattern(pattern); + var searchKey = this.HandleSearchKeyPattern(pattern); + + var count = await Task.Run(() => _cache.RemoveByPattern(searchKey, searchPattern), cancellationToken); + + if (_options.EnableLogging) + _logger?.LogInformation($"BaseRemoveByPatternAsync : pattern = {pattern} , count = {count}"); + } + /// /// Sets all async. /// @@ -337,4 +356,4 @@ public override Task BaseGetExpirationAsync(string cacheKey, Cancellat return Task.FromResult(_cache.GetExpiration(cacheKey)); } } -} \ No newline at end of file +} diff --git a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.cs b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.cs index 420b193d..7258cfc6 100644 --- a/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.cs +++ b/src/EasyCaching.InMemory/DefaultInMemoryCachingProvider.cs @@ -206,6 +206,7 @@ public override void BaseRemove(string cacheKey) _cache.Remove(cacheKey); } + /// /// Set the specified cacheKey, cacheValue and expiration. @@ -257,6 +258,24 @@ public override void BaseRemoveByPrefix(string prefix) if (_options.EnableLogging) _logger?.LogInformation($"RemoveByPrefix : prefix = {prefix} , count = {count}"); } + + /// + /// Removes cached items by pattern async. + /// + /// The by prefix async. + /// Pattern. + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + var searchPattern = this.ProcessSearchKeyPattern(pattern); + var searchKey = this.HandleSearchKeyPattern(pattern); + + var count = _cache.RemoveByPattern( searchKey, searchPattern); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPattern : pattern = {pattern} , count = {count}"); + } /// /// Sets all. @@ -379,4 +398,4 @@ public override TimeSpan BaseGetExpiration(string cacheKey) public override object BaseGetDatabse() => _cache; } -} \ No newline at end of file +} diff --git a/src/EasyCaching.InMemory/Internal/IInMemoryCaching.cs b/src/EasyCaching.InMemory/Internal/IInMemoryCaching.cs index 5fb3493a..3060b570 100644 --- a/src/EasyCaching.InMemory/Internal/IInMemoryCaching.cs +++ b/src/EasyCaching.InMemory/Internal/IInMemoryCaching.cs @@ -18,6 +18,7 @@ public interface IInMemoryCaching int RemoveAll(IEnumerable keys = null); bool Remove(string key); int RemoveByPrefix(string prefix); + int RemoveByPattern(string searchKey, SearchKeyPattern searchPattern); IDictionary> GetAll(IEnumerable keys); int SetAll(IDictionary values, TimeSpan? expiresIn = null); bool Replace(string key, T value, TimeSpan? expiresIn = null); diff --git a/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs b/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs index cba5f896..4a9ea1a7 100644 --- a/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs +++ b/src/EasyCaching.InMemory/Internal/InMemoryCaching.cs @@ -35,7 +35,7 @@ public void Clear(string prefix = "") { _memory.Clear(); - if (_options.SizeLimit.HasValue) + if (_options.SizeLimit.HasValue) Interlocked.Exchange(ref _cacheSize, 0); } else @@ -53,8 +53,8 @@ public int GetCount(string prefix = "") internal void RemoveExpiredKey(string key) { - bool flag = _memory.TryRemove(key, out _); - if (_options.SizeLimit.HasValue && flag) + bool flag = _memory.TryRemove(key, out _); + if (_options.SizeLimit.HasValue && flag) Interlocked.Decrement(ref _cacheSize); } @@ -79,7 +79,7 @@ public CacheValue Get(string key) return new CacheValue(value, true); } catch (Exception ex) - { + { System.Diagnostics.Debug.WriteLine($"some error herer, message = {ex.Message}"); return CacheValue.NoValue; } @@ -134,49 +134,49 @@ private bool SetInternal(CacheEntry entry, bool addOnly = false) return false; } - if (_options.SizeLimit.HasValue && Interlocked.Read(ref _cacheSize) >= _options.SizeLimit) - { - // prevent alaways access the following logic after up to limit - if (_memory.TryAdd(_UPTOLIMIT_KEY, new CacheEntry(_UPTOLIMIT_KEY, 1, DateTimeOffset.UtcNow.AddSeconds(5)))) - { - var shouldRemoveCount = 5; - - if (_options.SizeLimit.Value >= 10000) - { - shouldRemoveCount = (int)(_options.SizeLimit * 0.005d); - } - else if (_options.SizeLimit.Value >= 1000 && _options.SizeLimit.Value < 10000) - { - shouldRemoveCount = (int)(_options.SizeLimit * 0.01d); - } - - var oldestList = _memory.ToArray() - .OrderBy(kvp => kvp.Value.LastAccessTicks) - .ThenBy(kvp => kvp.Value.InstanceNumber) - .Take(shouldRemoveCount) - .Select(kvp => kvp.Key); - - RemoveAll(oldestList); - - //// this key will be remove by ScanForExpiredItems. - //_memory.TryRemove(_UPTOLIMIT_KEY, out _); + if (_options.SizeLimit.HasValue && Interlocked.Read(ref _cacheSize) >= _options.SizeLimit) + { + // prevent alaways access the following logic after up to limit + if (_memory.TryAdd(_UPTOLIMIT_KEY, new CacheEntry(_UPTOLIMIT_KEY, 1, DateTimeOffset.UtcNow.AddSeconds(5)))) + { + var shouldRemoveCount = 5; + + if (_options.SizeLimit.Value >= 10000) + { + shouldRemoveCount = (int)(_options.SizeLimit * 0.005d); + } + else if (_options.SizeLimit.Value >= 1000 && _options.SizeLimit.Value < 10000) + { + shouldRemoveCount = (int)(_options.SizeLimit * 0.01d); + } + + var oldestList = _memory.ToArray() + .OrderBy(kvp => kvp.Value.LastAccessTicks) + .ThenBy(kvp => kvp.Value.InstanceNumber) + .Take(shouldRemoveCount) + .Select(kvp => kvp.Key); + + RemoveAll(oldestList); + + //// this key will be remove by ScanForExpiredItems. + //_memory.TryRemove(_UPTOLIMIT_KEY, out _); } } CacheEntry deep = null; - if (_options.EnableWriteDeepClone) - { - try - { - deep = DeepClonerGenerator.CloneObject(entry); - } - catch (Exception) - { - deep = entry; - } + if (_options.EnableWriteDeepClone) + { + try + { + deep = DeepClonerGenerator.CloneObject(entry); + } + catch (Exception) + { + deep = entry; + } } else - { + { deep = entry; } @@ -195,17 +195,17 @@ private bool SetInternal(CacheEntry entry, bool addOnly = false) } else { - _memory.AddOrUpdate(deep.Key, deep, (k, cacheEntry) => deep); - - if (_options.SizeLimit.HasValue) + _memory.AddOrUpdate(deep.Key, deep, (k, cacheEntry) => deep); + + if (_options.SizeLimit.HasValue) Interlocked.Increment(ref _cacheSize); } StartScanForExpiredItems(); return true; - } - + } + public bool Exists(string key) { ArgumentCheck.NotNullOrWhiteSpace(key, nameof(key)); @@ -217,18 +217,18 @@ public int RemoveAll(IEnumerable keys = null) { if (keys == null) { - if (_options.SizeLimit.HasValue) - { - int count = (int)Interlocked.Read(ref _cacheSize); - Interlocked.Exchange(ref _cacheSize, 0); - _memory.Clear(); - return count; - } - else - { - int count = _memory.Count; - _memory.Clear(); - return count; + if (_options.SizeLimit.HasValue) + { + int count = (int)Interlocked.Read(ref _cacheSize); + Interlocked.Exchange(ref _cacheSize, 0); + _memory.Clear(); + return count; + } + else + { + int count = _memory.Count; + _memory.Clear(); + return count; } } @@ -238,11 +238,11 @@ public int RemoveAll(IEnumerable keys = null) if (string.IsNullOrEmpty(key)) continue; - if (_memory.TryRemove(key, out _)) + if (_memory.TryRemove(key, out _)) { - removed++; - if (_options.SizeLimit.HasValue) - Interlocked.Decrement(ref _cacheSize); + removed++; + if (_options.SizeLimit.HasValue) + Interlocked.Decrement(ref _cacheSize); } } @@ -250,12 +250,12 @@ public int RemoveAll(IEnumerable keys = null) } public bool Remove(string key) - { - bool flag = _memory.TryRemove(key, out _); - - if (_options.SizeLimit.HasValue && !key.Equals(_UPTOLIMIT_KEY) && flag) - { - Interlocked.Decrement(ref _cacheSize); + { + bool flag = _memory.TryRemove(key, out _); + + if (_options.SizeLimit.HasValue && !key.Equals(_UPTOLIMIT_KEY) && flag) + { + Interlocked.Decrement(ref _cacheSize); } return flag; @@ -267,6 +267,30 @@ public int RemoveByPrefix(string prefix) return RemoveAll(keysToRemove); } + public int RemoveByPattern(string searchKey, SearchKeyPattern searchPattern) + { + var keysToRemove = _memory.Keys.Where(x => FilterByPattern(x, searchKey, searchPattern)).ToList(); + + return RemoveAll(keysToRemove); + } + + private static bool FilterByPattern(string key, string searchKey, SearchKeyPattern searchKeyPattern) + { + switch (searchKeyPattern) + { + case SearchKeyPattern.Postfix: + return key.EndsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Prefix: + return key.StartsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Contains: + return key.Contains(searchKey); + case SearchKeyPattern.Exact: + return key.Equals(searchKey, StringComparison.Ordinal); + default: + throw new ArgumentOutOfRangeException(nameof(searchKeyPattern), searchKeyPattern, null); + } + } + public IDictionary> GetAll(IEnumerable keys) { var map = new Dictionary>(); @@ -378,16 +402,16 @@ internal object Value /// public T GetValue(bool isDeepClone = true) { - object val = Value; + object val = Value; var t = typeof(T); if (t == TypeHelper.BoolType || t == TypeHelper.StringType || t == TypeHelper.CharType || t == TypeHelper.DateTimeType || t.IsNumeric()) return (T)Convert.ChangeType(val, t); - if (t == TypeHelper.NullableBoolType || t == TypeHelper.NullableCharType || t == TypeHelper.NullableDateTimeType || t.IsNullableNumeric()) - return val == null ? default(T) : (T)Convert.ChangeType(val, Nullable.GetUnderlyingType(t)); - + if (t == TypeHelper.NullableBoolType || t == TypeHelper.NullableCharType || t == TypeHelper.NullableDateTimeType || t.IsNullableNumeric()) + return val == null ? default(T) : (T)Convert.ChangeType(val, Nullable.GetUnderlyingType(t)); + return isDeepClone ? DeepClonerGenerator.CloneObject((T)val) : (T)val; diff --git a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs index 9e866ce6..85e13da0 100644 --- a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs +++ b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.Async.cs @@ -129,6 +129,21 @@ public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationTo await Task.Run(() => BaseRemoveByPrefix(prefix), cancellationToken); } + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPatternAsync : pattern = {pattern}"); + + await Task.Run(() => BaseRemoveByPattern(pattern), cancellationToken); + } + /// /// Sets all async. /// @@ -217,4 +232,4 @@ public override async Task BaseGetExpirationAsync(string cacheKey, Can return await Task.Run(() => BaseGetExpiration(cacheKey), cancellationToken); } } -} \ No newline at end of file +} diff --git a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs index eeea279d..9531e069 100644 --- a/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs +++ b/src/EasyCaching.LiteDB/DefaultLiteDBCachingProvider.cs @@ -1,4 +1,5 @@ -namespace EasyCaching.LiteDB + +namespace EasyCaching.LiteDB { using EasyCaching.Core; using global::LiteDB; @@ -6,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; + using System.Linq.Expressions; /// /// LiteDBCaching provider. @@ -129,8 +131,8 @@ public override CacheValue BaseGet(string cacheKey, Func dataRetriever, ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); - + var cacheItem = _cache.FindOne(c => c.cachekey == cacheKey && c.expiration > DateTimeOffset.Now.ToUnixTimeSeconds()); + if (cacheItem != null) { if (_options.EnableLogging) @@ -147,8 +149,8 @@ public override CacheValue BaseGet(string cacheKey, Func dataRetriever, { Set(cacheKey, item, expiration); return new CacheValue(item, true); - } - + } + return CacheValue.NoValue; } @@ -196,6 +198,7 @@ public override void BaseRemove(string cacheKey) ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); _cache.DeleteMany(c => c.cachekey == cacheKey); } + /// /// Set the specified cacheKey, cacheValue and expiration. @@ -221,7 +224,7 @@ public override void BaseSet(string cacheKey, T cacheValue, TimeSpan expirati cachekey = cacheKey, name = _name, cachevalue = Newtonsoft.Json.JsonConvert.SerializeObject(cacheValue), - expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() + expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() }); } @@ -238,6 +241,40 @@ public override void BaseRemoveByPrefix(string prefix) _cache.DeleteMany(c => c.cachekey.StartsWith(prefix)); } + + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPattern : pattern = {pattern}"); + + var searchPattern = this.ProcessSearchKeyPattern(pattern); + var searchKey = this.HandleSearchKeyPattern(pattern); + + _cache.DeleteMany(FilterByPattern(searchKey, searchPattern)); + } + + private static Expression> FilterByPattern(string searchKey, SearchKeyPattern searchKeyPattern) + { + switch (searchKeyPattern) + { + case SearchKeyPattern.Postfix: + return item => item.cachekey.EndsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Prefix: + return item => item.cachekey.StartsWith(searchKey, StringComparison.Ordinal); + case SearchKeyPattern.Contains: + return item => item.cachekey.Contains(searchKey); + case SearchKeyPattern.Exact: + return item => item.cachekey.Equals(searchKey, StringComparison.Ordinal); + default: + throw new ArgumentOutOfRangeException(nameof(searchKeyPattern), searchKeyPattern, null); + } + } /// /// Sets all. @@ -259,7 +296,7 @@ public override void BaseSetAll(IDictionary values, TimeSpan expir cachekey = item.Key, name = _name, cachevalue = Newtonsoft.Json.JsonConvert.SerializeObject(item.Value), - expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() + expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() }); } _litedb.Commit(); @@ -380,7 +417,7 @@ public override bool BaseTrySet(string cacheKey, T cacheValue, TimeSpan expir cachekey = cacheKey, name = _name, cachevalue = Newtonsoft.Json.JsonConvert.SerializeObject(cacheValue), - expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() + expiration = DateTimeOffset.UtcNow.Add(expiration).ToUnixTimeSeconds() }); result = rows != null; } @@ -408,4 +445,4 @@ public override TimeSpan BaseGetExpiration(string cacheKey) public override object BaseGetDatabse() => _cache; } -} \ No newline at end of file +} diff --git a/src/EasyCaching.Memcached/Configurations/EasyCachingMemcachedClientConfiguration.cs b/src/EasyCaching.Memcached/Configurations/EasyCachingMemcachedClientConfiguration.cs index edb9db5a..dc14f2d3 100644 --- a/src/EasyCaching.Memcached/Configurations/EasyCachingMemcachedClientConfiguration.cs +++ b/src/EasyCaching.Memcached/Configurations/EasyCachingMemcachedClientConfiguration.cs @@ -38,6 +38,7 @@ public EasyCachingMemcachedClientConfiguration( var options = optionsAccessor.DBConfig; ConfigureServers(options); + UseSslStream = options.UseSslStream; SocketPool = new SocketPoolConfiguration(); if (options.SocketPool != null) @@ -182,6 +183,11 @@ private void ConfigureServers(EasyCachingMemcachedClientOptions options) /// public IAuthenticationConfiguration Authentication { get; private set; } + /// + /// Gets the SSL Stream support. + /// + public bool UseSslStream { get; private set; } + /// /// Gets or sets the which will be used to convert item keys for Memcached. /// diff --git a/src/EasyCaching.Memcached/Configurations/MemcachedOptionsExtension.cs b/src/EasyCaching.Memcached/Configurations/MemcachedOptionsExtension.cs index b27ee6a1..c9aa45cc 100644 --- a/src/EasyCaching.Memcached/Configurations/MemcachedOptionsExtension.cs +++ b/src/EasyCaching.Memcached/Configurations/MemcachedOptionsExtension.cs @@ -49,7 +49,6 @@ public void AddServices(IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); services.AddSingleton(x => { var optionsMon = x.GetRequiredService>(); diff --git a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs index 5ab09d71..a3287de7 100644 --- a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs +++ b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.Async.cs @@ -218,6 +218,11 @@ await _memcachedClient.StoreAsync( new TimeSpan(0, 0, 0)); } + public override Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + throw new NotSupportedException("RemoveByPattern is not supported in memcached provider."); + } + /// /// Sets all async. /// @@ -333,4 +338,4 @@ public override Task BaseGetExpirationAsync(string cacheKey, Cancellat throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs index 97455267..e380ab3c 100644 --- a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs +++ b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs @@ -263,6 +263,11 @@ public override void BaseRemoveByPrefix(string prefix) CheckResult(data); } + + public override void BaseRemoveByPattern(string pattern) + { + throw new NotSupportedException("RemoveByPattern is not supported in memcached provider."); + } /// /// Handle the cache key of memcached limititaion @@ -451,4 +456,4 @@ private void CheckResult(Enyim.Caching.Memcached.Results.IOperationResult data) throw new EasyCachingException($"opereation fail, {data.InnerResult?.Message ?? ""}", data.Exception); } } -} \ No newline at end of file +} diff --git a/src/EasyCaching.Memcached/EasyCaching.Memcached.csproj b/src/EasyCaching.Memcached/EasyCaching.Memcached.csproj index fccb6f1f..b6029510 100644 --- a/src/EasyCaching.Memcached/EasyCaching.Memcached.csproj +++ b/src/EasyCaching.Memcached/EasyCaching.Memcached.csproj @@ -35,6 +35,6 @@ - + diff --git a/src/EasyCaching.Redis/Configurations/RedisOptionsExtension.cs b/src/EasyCaching.Redis/Configurations/RedisOptionsExtension.cs index dbe069ea..616142f8 100755 --- a/src/EasyCaching.Redis/Configurations/RedisOptionsExtension.cs +++ b/src/EasyCaching.Redis/Configurations/RedisOptionsExtension.cs @@ -44,8 +44,6 @@ public RedisOptionsExtension(string name, Action configure) public void AddServices(IServiceCollection services) { services.AddOptions(); - - services.TryAddSingleton(); services.Configure(_name, configure); diff --git a/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs b/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs index 3fffbfdb..8e967c75 100644 --- a/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs +++ b/src/EasyCaching.Redis/DefaultRedisCachingProvider.Async.cs @@ -229,6 +229,25 @@ public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationTo await _cache.KeyDeleteAsync(redisKeys); } + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + pattern = this.HandleKeyPattern(pattern); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPatternAsync : pattern = {pattern}"); + + var redisKeys = this.SearchRedisKeys(pattern); + + await _cache.KeyDeleteAsync(redisKeys); + } + /// /// Sets all async. /// diff --git a/src/EasyCaching.Redis/DefaultRedisCachingProvider.cs b/src/EasyCaching.Redis/DefaultRedisCachingProvider.cs index fcf9795c..f3836fb6 100644 --- a/src/EasyCaching.Redis/DefaultRedisCachingProvider.cs +++ b/src/EasyCaching.Redis/DefaultRedisCachingProvider.cs @@ -229,6 +229,7 @@ public override void BaseRemove(string cacheKey) _cache.KeyDelete(cacheKey); } + /// /// Set the specified cacheKey, cacheValue and expiration. @@ -285,6 +286,24 @@ public override void BaseRemoveByPrefix(string prefix) _cache.KeyDelete(redisKeys); } + + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + pattern = this.HandleKeyPattern(pattern); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPattern : pattern = {pattern}"); + + var redisKeys = this.SearchRedisKeys(pattern); + + _cache.KeyDelete(redisKeys); + } /// /// Searchs the redis keys. @@ -356,7 +375,23 @@ private string HandlePrefix(string prefix) return prefix; } + + /// + /// Handles the pattern of CacheKey. + /// + /// Pattern of CacheKey. + private string HandleKeyPattern(string pattern) + { + // Forbid + if (pattern.Equals("*")) + throw new ArgumentException("the pattern should not equal to *"); + + if (!string.IsNullOrWhiteSpace(_options.DBConfig.KeyPrefix)) + pattern = _options.DBConfig.KeyPrefix + pattern; + return pattern; + } + /// /// Sets all. /// diff --git a/src/EasyCaching.SQLite/Configurations/ConstSQL.cs b/src/EasyCaching.SQLite/Configurations/ConstSQL.cs index d54bccf0..948160a1 100644 --- a/src/EasyCaching.SQLite/Configurations/ConstSQL.cs +++ b/src/EasyCaching.SQLite/Configurations/ConstSQL.cs @@ -69,9 +69,9 @@ FROM [easycaching] public const string REMOVESQL = @"DELETE FROM [easycaching] WHERE [cachekey] = @cachekey AND [name] = @name "; /// - /// The removebyprefixsql. + /// The removebylikesql. /// - public const string REMOVEBYPREFIXSQL = @"DELETE FROM [easycaching] WHERE [cachekey] like @cachekey AND [name]=@name"; + public const string REMOVEBYLIKESQL = @"DELETE FROM [easycaching] WHERE [cachekey] like @cachekey AND [name]=@name"; /// /// The existssql. diff --git a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs index 6354aa87..c4973ceb 100644 --- a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs +++ b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.Async.cs @@ -237,13 +237,32 @@ public override async Task BaseRemoveByPrefixAsync(string prefix, CancellationTo if (_options.EnableLogging) _logger?.LogInformation($"RemoveByPrefixAsync : prefix = {prefix}"); - await _cache.ExecuteAsync(new CommandDefinition(ConstSQL.REMOVEBYPREFIXSQL, new + await _cache.ExecuteAsync(new CommandDefinition(ConstSQL.REMOVEBYLIKESQL, new { cachekey = string.Concat(prefix, "%"), name = _name }, cancellationToken: cancellationToken)); } + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + /// CancellationToken + public override async Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPatternAsync : pattern = {pattern}"); + + await _cache.ExecuteAsync(new CommandDefinition(ConstSQL.REMOVEBYLIKESQL, new + { + cachekey = pattern.Replace('*', '%'), + name = _name + }, cancellationToken: cancellationToken)); + } + /// /// Sets all async. /// diff --git a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.cs b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.cs index 7610a3f2..47c178d1 100644 --- a/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.cs +++ b/src/EasyCaching.SQLite/DefaultSQLiteCachingProvider.cs @@ -234,9 +234,23 @@ public override void BaseRemoveByPrefix(string prefix) if (_options.EnableLogging) _logger?.LogInformation($"RemoveByPrefix : prefix = {prefix}"); - _cache.Execute(ConstSQL.REMOVEBYPREFIXSQL, new { cachekey = string.Concat(prefix, "%"), name = _name }); + _cache.Execute(ConstSQL.REMOVEBYLIKESQL, new { cachekey = string.Concat(prefix, "%"), name = _name }); } + /// + /// Removes cached item by pattern async. + /// + /// Pattern of CacheKey. + public override void BaseRemoveByPattern(string pattern) + { + ArgumentCheck.NotNullOrWhiteSpace(pattern, nameof(pattern)); + + if (_options.EnableLogging) + _logger?.LogInformation($"RemoveByPattern : pattern = {pattern}"); + + _cache.Execute(ConstSQL.REMOVEBYLIKESQL, new { cachekey = pattern.Replace('*', '%'), name = _name }); + } + /// /// Sets all. /// diff --git a/test/EasyCaching.PerformanceTests/SerializerBenchmark.cs b/test/EasyCaching.PerformanceTests/SerializerBenchmark.cs index e1d2a2bd..158ba8da 100644 --- a/test/EasyCaching.PerformanceTests/SerializerBenchmark.cs +++ b/test/EasyCaching.PerformanceTests/SerializerBenchmark.cs @@ -18,7 +18,6 @@ public abstract class SerializerBenchmark private DefaultJsonSerializer _json = new DefaultJsonSerializer("json", new JsonSerializerSettings()); private DefaultMessagePackSerializer _messagepack = new DefaultMessagePackSerializer("msgpack", new EasyCachingMsgPackSerializerOptions { }); private DefaultProtobufSerializer _protobuf = new DefaultProtobufSerializer("proto"); - private DefaultBinaryFormatterSerializer _binary = new DefaultBinaryFormatterSerializer(); protected MyPoco _single; protected List _list; private int _count; @@ -39,12 +38,6 @@ public void Setup() _list = items; } - [Benchmark] - public void BinaryFormatter() - { - Exec(_binary); - } - [Benchmark] public void Json() { diff --git a/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs index b937e41c..ce00bdf9 100644 --- a/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/BaseCachingProviderTest.cs @@ -868,6 +868,132 @@ protected virtual async Task RemoveByPrefixAsync_Should_Succeed() Assert.False(demo4.HasValue); Assert.True(xxx1.HasValue); } + + #endregion + + #region RemoveByPattern/RemoveByPatternAsync + + [Fact] + public virtual void RemoveByPattern_Should_Succeed() + { + SetCacheItem("garden:pots:flowers", "ok"); + SetCacheItem("garden:pots:flowers:test", "ok"); + SetCacheItem("garden:flowerspots:test", "ok" ); + SetCacheItem("boo:foo", "ok"); + SetCacheItem("boo:test:foo", "ok"); + SetCacheItem("sky:birds:bar", "ok"); + SetCacheItem("sky:birds:test:bar", "ok"); + SetCacheItem("akey", "ok"); + + var val1 = _provider.Get("garden:pots:flowers"); + var val2 = _provider.Get("garden:pots:flowers:test"); + var val3 = _provider.Get("garden:flowerspots:test"); + var val4 = _provider.Get("boo:foo"); + var val5 = _provider.Get("boo:test:foo"); + var val6 = _provider.Get("sky:birds:bar"); + var val7 = _provider.Get("sky:birds:test:bar"); + var val8 = _provider.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + _provider.RemoveByPattern("*:pots:*"); + + // postfix + _provider.RemoveByPattern("*foo"); + + // prefix + _provider.RemoveByPattern("sky*"); + + // exact + _provider.RemoveByPattern("akey"); + + var val9 = _provider.Get("garden:pots:flowers"); + var val10 = _provider.Get("garden:pots:flowers:test"); + var val11 = _provider.Get("garden:flowerspots:test"); + var val12 = _provider.Get("boo:foo"); + var val13 = _provider.Get("boo:test:foo"); + var val14 = _provider.Get("sky:birds:bar"); + var val15 = _provider.Get("sky:birds:test:bar"); + var val16 = _provider.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } + + [Fact] + public virtual async Task RemoveByPatternAsync_Should_Succeed() + { + SetCacheItem("garden:pots:flowers", "ok"); + SetCacheItem("garden:pots:flowers:test", "ok"); + SetCacheItem("garden:flowerspots:test", "ok" ); + SetCacheItem("boo:foo", "ok"); + SetCacheItem("boo:test:foo", "ok"); + SetCacheItem("sky:birds:bar", "ok"); + SetCacheItem("sky:birds:test:bar", "ok"); + SetCacheItem("akey", "ok"); + + var val1 = _provider.Get("garden:pots:flowers"); + var val2 = _provider.Get("garden:pots:flowers:test"); + var val3 = _provider.Get("garden:flowerspots:test"); + var val4 = _provider.Get("boo:foo"); + var val5 = _provider.Get("boo:test:foo"); + var val6 = _provider.Get("sky:birds:bar"); + var val7 = _provider.Get("sky:birds:test:bar"); + var val8 = _provider.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + await _provider.RemoveByPatternAsync("*:pots:*"); + + // postfix + await _provider.RemoveByPatternAsync("*foo"); + + // prefix + await _provider.RemoveByPatternAsync("sky*"); + + // exact + await _provider.RemoveByPatternAsync("akey"); + + var val9 = _provider.Get("garden:pots:flowers"); + var val10 = _provider.Get("garden:pots:flowers:test"); + var val11 = _provider.Get("garden:flowerspots:test"); + var val12 = _provider.Get("boo:foo"); + var val13 = _provider.Get("boo:test:foo"); + var val14 = _provider.Get("sky:birds:bar"); + var val15 = _provider.Get("sky:birds:test:bar"); + var val16 = _provider.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } #endregion #region SetAll/SetAllAsync @@ -1426,4 +1552,4 @@ protected virtual void OnHit_Should_Return_Zero_And_OnMiss_Should_Return_One() Assert.Equal(1, missedRes); } } -} \ No newline at end of file +} diff --git a/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs index 8e132070..c90a2343 100644 --- a/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/CSRedisCachingProviderTest.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + namespace EasyCaching.UnitTests { using System; @@ -146,8 +148,7 @@ public CSRedisCachingProviderWithKeyPrefixTest() config.SerializerName = "json"; }, "WithKeyPrefix"); - - + x.WithJson("json"); }); @@ -193,5 +194,135 @@ public void RemoveByPrefixTest() Assert.False(val3.HasValue); Assert.False(val4.HasValue); } + + [Theory] + [InlineData("WithKeyPrefix")] + [InlineData("NotKeyPrefix")] + public void RemoveByKeyPatternTest(string provider) + { + var WithKeyPrefix = _providerFactory.GetCachingProvider(provider); + + WithKeyPrefix.Set("garden:pots:flowers", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("garden:pots:flowers:test", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("garden:flowerspots:test", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("boo:foo", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("boo:test:foo", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("sky:birds:bar", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("sky:birds:test:bar", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("akey", "ok", TimeSpan.FromSeconds(10)); + + var val1 = WithKeyPrefix.Get("garden:pots:flowers"); + var val2 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val3 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val4 = WithKeyPrefix.Get("boo:foo"); + var val5 = WithKeyPrefix.Get("boo:test:foo"); + var val6 = WithKeyPrefix.Get("sky:birds:bar"); + var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val8 = WithKeyPrefix.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + WithKeyPrefix.RemoveByPattern("*:pots:*"); + + // postfix + WithKeyPrefix.RemoveByPattern("*foo"); + + // prefix + WithKeyPrefix.RemoveByPattern("sky*"); + + // exact + WithKeyPrefix.RemoveByPattern("akey"); + + var val9 = WithKeyPrefix.Get("garden:pots:flowers"); + var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val11 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val12 = WithKeyPrefix.Get("boo:foo"); + var val13 = WithKeyPrefix.Get("boo:test:foo"); + var val14 = WithKeyPrefix.Get("sky:birds:bar"); + var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val16 = WithKeyPrefix.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } + + [Theory] + [InlineData("WithKeyPrefix")] + [InlineData("NotKeyPrefix")] + public async Task RemoveByKeyPatternAsyncTest(string provider) + { + var WithKeyPrefix = _providerFactory.GetCachingProvider(provider); + + await WithKeyPrefix.SetAsync("garden:pots:flowers", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("garden:pots:flowers:test", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("garden:flowerspots:test", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("boo:foo", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("boo:test:foo", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("sky:birds:bar", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("sky:birds:test:bar", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("akey", "ok", TimeSpan.FromSeconds(10)); + + var val1 = WithKeyPrefix.Get("garden:pots:flowers"); + var val2 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val3 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val4 = WithKeyPrefix.Get("boo:foo"); + var val5 = WithKeyPrefix.Get("boo:test:foo"); + var val6 = WithKeyPrefix.Get("sky:birds:bar"); + var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val8 = WithKeyPrefix.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + await WithKeyPrefix.RemoveByPatternAsync("*:pots:*"); + + // postfix + await WithKeyPrefix.RemoveByPatternAsync("*foo"); + + // prefix + await WithKeyPrefix.RemoveByPatternAsync("sky*"); + + // exact + await WithKeyPrefix.RemoveByPatternAsync("akey"); + + var val9 = WithKeyPrefix.Get("garden:pots:flowers"); + var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val11 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val12 = WithKeyPrefix.Get("boo:foo"); + var val13 = WithKeyPrefix.Get("boo:test:foo"); + var val14 = WithKeyPrefix.Get("sky:birds:bar"); + var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val16 = WithKeyPrefix.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } } } diff --git a/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs index 035efdb3..0d5cf30a 100644 --- a/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs @@ -24,8 +24,9 @@ protected override IEasyCachingProvider CreateCachingProvider(Action { options.DBConfig.AddServer("127.0.0.1", 11211); + options.SerializerName = "msg"; additionalSetup(options); - }).UseMemcachedLock()); + }).WithMessagePack("msg").UseMemcachedLock()); services.AddLogging(); IServiceProvider serviceProvider = services.BuildServiceProvider(); return serviceProvider.GetService(); @@ -114,6 +115,17 @@ protected override void GetByPrefix_Should_Succeed() { } + [Fact] + public override void RemoveByPattern_Should_Succeed() + { + } + + [Fact] + public override async Task RemoveByPatternAsync_Should_Succeed() + { + await Task.FromResult(1); + } + [Fact] protected override async Task GetByPrefixAsync_Should_Succeed() { @@ -212,9 +224,18 @@ public MemcachedProviderWithFactoryTest() IServiceCollection services = new ServiceCollection(); services.AddEasyCaching(x => { - x.UseMemcached(options => { options.DBConfig.AddServer("127.0.0.1", 11212); }, SECOND_PROVIDER_NAME) + x.WithMessagePack("msg"); + x.UseMemcached(options => + { + options.DBConfig.AddServer("127.0.0.1", 11212); + options.SerializerName = "msg"; + }, SECOND_PROVIDER_NAME) .UseMemcached( - options => { options.DBConfig.AddServer("127.0.0.1", 11211); }, "MyTest"); + options => + { + options.DBConfig.AddServer("127.0.0.1", 11211); + options.SerializerName = "msg"; + }, "MyTest"); }); services.AddLogging(); IServiceProvider serviceProvider = services.BuildServiceProvider(); @@ -335,6 +356,7 @@ public MemcachedProviderUseEasyCachingTest() services.AddLogging(); services.AddEasyCaching(option => { + option.WithMessagePack("msg"); option.UseMemcached(config => { config.DBConfig = new EasyCachingMemcachedClientOptions @@ -344,6 +366,7 @@ public MemcachedProviderUseEasyCachingTest() new Enyim.Caching.Configuration.Server() {Address = "127.0.0.1", Port = 11212} } }; + config.SerializerName = "msg"; }, EasyCachingConstValue.DefaultMemcachedName); }); @@ -361,7 +384,7 @@ public MemcachedProviderUseEasyCachingWithConfigTest() { IServiceCollection services = new ServiceCollection(); - var appsettings = " { \"easycaching\": { \"memcached\": { \"MaxRdSecond\": 600, \"dbconfig\": { \"Servers\": [ { \"Address\": \"127.0.0.1\", \"Port\": 11211 } ] } } }}"; + var appsettings = " {\"easycaching\":{\"memcached\":{\"MaxRdSecond\":600,\"dbconfig\":{\"Servers\":[{\"Address\":\"127.0.0.1\",\"Port\":11211}]},\"SerializerName\":\"msg\"}}} "; var path = TestHelpers.CreateTempFile(appsettings); var directory = Path.GetDirectoryName(path); var fileName = Path.GetFileName(path); @@ -371,7 +394,11 @@ public MemcachedProviderUseEasyCachingWithConfigTest() configurationBuilder.AddJsonFile(fileName); var config = configurationBuilder.Build(); services.AddLogging(); - services.AddEasyCaching(option => { option.UseMemcached(config, "mName"); }); + services.AddEasyCaching(option => + { + option.WithMessagePack("msg"); + option.UseMemcached(config, "mName"); + }); IServiceProvider serviceProvider = services.BuildServiceProvider(); _provider = serviceProvider.GetService(); @@ -396,6 +423,7 @@ public async void NoConnectionTest() services.AddLogging(); services.AddEasyCaching(option => { + option.WithMessagePack("msg"); option.UseMemcached(config => { config.DBConfig = new EasyCachingMemcachedClientOptions @@ -411,6 +439,7 @@ public async void NoConnectionTest() ReceiveTimeout = TimeSpan.FromSeconds(2), } }; + config.SerializerName = "msg"; }, EasyCachingConstValue.DefaultMemcachedName); }); @@ -424,4 +453,4 @@ public async void NoConnectionTest() await Assert.ThrowsAnyAsync(() => provider.RemoveAsync("123123")); } } -} \ No newline at end of file +} diff --git a/test/EasyCaching.UnitTests/CachingTests/RedisCachingProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/RedisCachingProviderTest.cs index 1cb92953..28463ed2 100644 --- a/test/EasyCaching.UnitTests/CachingTests/RedisCachingProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/RedisCachingProviderTest.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + namespace EasyCaching.UnitTests { using EasyCaching.Core; @@ -286,5 +288,135 @@ public void RemoveByPrefixTest() Assert.False(val3.HasValue); Assert.False(val4.HasValue); } + + [Theory] + [InlineData("WithKeyPrefix")] + [InlineData("NotKeyPrefix")] + public void RemoveByKeyPatternTest(string provider) + { + var WithKeyPrefix = _providerFactory.GetCachingProvider(provider); + + WithKeyPrefix.Set("garden:pots:flowers", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("garden:pots:flowers:test", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("garden:flowerspots:test", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("boo:foo", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("boo:test:foo", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("sky:birds:bar", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("sky:birds:test:bar", "ok", TimeSpan.FromSeconds(10)); + WithKeyPrefix.Set("akey", "ok", TimeSpan.FromSeconds(10)); + + var val1 = WithKeyPrefix.Get("garden:pots:flowers"); + var val2 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val3 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val4 = WithKeyPrefix.Get("boo:foo"); + var val5 = WithKeyPrefix.Get("boo:test:foo"); + var val6 = WithKeyPrefix.Get("sky:birds:bar"); + var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val8 = WithKeyPrefix.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + WithKeyPrefix.RemoveByPattern("*:pots:*"); + + // postfix + WithKeyPrefix.RemoveByPattern("*foo"); + + // prefix + WithKeyPrefix.RemoveByPattern("sky*"); + + // exact + WithKeyPrefix.RemoveByPattern("akey"); + + var val9 = WithKeyPrefix.Get("garden:pots:flowers"); + var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val11 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val12 = WithKeyPrefix.Get("boo:foo"); + var val13 = WithKeyPrefix.Get("boo:test:foo"); + var val14 = WithKeyPrefix.Get("sky:birds:bar"); + var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val16 = WithKeyPrefix.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } + + [Theory] + [InlineData("WithKeyPrefix")] + [InlineData("NotKeyPrefix")] + public async Task RemoveByKeyPatternAsyncTest(string provider) + { + var WithKeyPrefix = _providerFactory.GetCachingProvider(provider); + + await WithKeyPrefix.SetAsync("garden:pots:flowers", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("garden:pots:flowers:test", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("garden:flowerspots:test", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("boo:foo", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("boo:test:foo", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("sky:birds:bar", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("sky:birds:test:bar", "ok", TimeSpan.FromSeconds(10)); + await WithKeyPrefix.SetAsync("akey", "ok", TimeSpan.FromSeconds(10)); + + var val1 = WithKeyPrefix.Get("garden:pots:flowers"); + var val2 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val3 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val4 = WithKeyPrefix.Get("boo:foo"); + var val5 = WithKeyPrefix.Get("boo:test:foo"); + var val6 = WithKeyPrefix.Get("sky:birds:bar"); + var val7 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val8 = WithKeyPrefix.Get("akey"); + + Assert.True(val1.HasValue); + Assert.True(val2.HasValue); + Assert.True(val3.HasValue); + Assert.True(val4.HasValue); + Assert.True(val5.HasValue); + Assert.True(val6.HasValue); + Assert.True(val7.HasValue); + Assert.True(val8.HasValue); + + // contains + await WithKeyPrefix.RemoveByPatternAsync("*:pots:*"); + + // postfix + await WithKeyPrefix.RemoveByPatternAsync("*foo"); + + // prefix + await WithKeyPrefix.RemoveByPatternAsync("sky*"); + + // exact + await WithKeyPrefix.RemoveByPatternAsync("akey"); + + var val9 = WithKeyPrefix.Get("garden:pots:flowers"); + var val10 = WithKeyPrefix.Get("garden:pots:flowers:test"); + var val11 = WithKeyPrefix.Get("garden:flowerspots:test"); + var val12 = WithKeyPrefix.Get("boo:foo"); + var val13 = WithKeyPrefix.Get("boo:test:foo"); + var val14 = WithKeyPrefix.Get("sky:birds:bar"); + var val15 = WithKeyPrefix.Get("sky:birds:test:bar"); + var val16 = WithKeyPrefix.Get("akey"); + + Assert.False(val9.HasValue); + Assert.False(val10.HasValue); + Assert.True(val11.HasValue); + Assert.False(val12.HasValue); + Assert.False(val13.HasValue); + Assert.False(val14.HasValue); + Assert.False(val15.HasValue); + Assert.False(val16.HasValue); + } } -} \ No newline at end of file +} diff --git a/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs b/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs index 698d10f7..84556c6c 100644 --- a/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Diagnostics/MyCachingProvider.cs @@ -2,8 +2,8 @@ { using EasyCaching.Core; using System; - using System.Collections.Generic; - using System.Threading; + using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; public class MyCachingProvider : EasyCachingAbstractProvider @@ -85,13 +85,13 @@ public override Task>> BaseGetByPrefixAsync public override int BaseGetCount(string prefix = "") { return 1; - } - - public override Task BaseGetCountAsync(string prefix = "", CancellationToken cancellationToken = default) - { - return Task.FromResult(1); - } - + } + + public override Task BaseGetCountAsync(string prefix = "", CancellationToken cancellationToken = default) + { + return Task.FromResult(1); + } + public override TimeSpan BaseGetExpiration(string cacheKey) { return TimeSpan.FromSeconds(1); @@ -137,6 +137,15 @@ public override Task BaseRemoveByPrefixAsync(string prefix, CancellationToken ca return Task.CompletedTask; } + public override void BaseRemoveByPattern(string pattern) + { + } + + public override Task BaseRemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + public override void BaseSet(string cacheKey, T cacheValue, TimeSpan expiration) { diff --git a/test/EasyCaching.UnitTests/DistributedLock/MemcachedLockTest.cs b/test/EasyCaching.UnitTests/DistributedLock/MemcachedLockTest.cs index 75e56385..4bc9b589 100644 --- a/test/EasyCaching.UnitTests/DistributedLock/MemcachedLockTest.cs +++ b/test/EasyCaching.UnitTests/DistributedLock/MemcachedLockTest.cs @@ -12,8 +12,12 @@ public class MemcachedLockTest : DistributedLockTest { private static readonly IDistributedLockFactory Factory = new ServiceCollection() .AddLogging() - .AddEasyCaching(option => option.UseMemcached(config => + .AddEasyCaching(option => + { + option.WithMessagePack("msg"); + option.UseMemcached(config => { + config.SerializerName = "msg"; config.DBConfig = new EasyCachingMemcachedClientOptions { Servers = @@ -21,8 +25,9 @@ public class MemcachedLockTest : DistributedLockTest new Server { Address = "127.0.0.1", Port = 11211 } } }; - }) - .UseMemcachedLock()) + }); + option.UseMemcachedLock(); + }) .BuildServiceProvider() .GetService(); diff --git a/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs b/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs index 8d350618..a059593f 100644 --- a/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Fake/FakeDistributedCachingProvider.cs @@ -1,8 +1,8 @@ namespace EasyCaching.UnitTests { using System; - using System.Collections.Generic; - using System.Threading; + using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using EasyCaching.Core; @@ -94,13 +94,13 @@ public virtual Task>> GetByPrefixAsync(stri public virtual int GetCount(string prefix = "") { return 1; - } - - public Task GetCountAsync(string prefix = "", CancellationToken cancellationToken = default) - { - return Task.FromResult(1); - } - + } + + public Task GetCountAsync(string prefix = "", CancellationToken cancellationToken = default) + { + return Task.FromResult(1); + } + public virtual TimeSpan GetExpiration(string cacheKey) { return TimeSpan.FromSeconds(1); @@ -151,6 +151,15 @@ public virtual void RemoveByPrefix(string prefix) } + public Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void RemoveByPattern(string pattern) + { + } + public virtual Task RemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default) { return Task.CompletedTask; diff --git a/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs b/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs index a2bead29..ebc6275b 100644 --- a/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs +++ b/test/EasyCaching.UnitTests/Fake/FakeLocalCachingProvider.cs @@ -1,8 +1,8 @@ namespace EasyCaching.UnitTests { using System; - using System.Collections.Generic; - using System.Threading; + using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using EasyCaching.Core; @@ -94,13 +94,13 @@ public Task>> GetByPrefixAsync(string prefi public int GetCount(string prefix = "") { return 1; - } - - public Task GetCountAsync(string prefix = "", CancellationToken cancellationToken = default) - { - return Task.FromResult(1); - } - + } + + public Task GetCountAsync(string prefix = "", CancellationToken cancellationToken = default) + { + return Task.FromResult(1); + } + public TimeSpan GetExpiration(string cacheKey) { return TimeSpan.FromSeconds(1); @@ -151,6 +151,15 @@ public void RemoveByPrefix(string prefix) } + public Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void RemoveByPattern(string pattern) + { + } + public Task RemoveByPrefixAsync(string prefix, CancellationToken cancellationToken = default) { return Task.CompletedTask; diff --git a/test/EasyCaching.UnitTests/SerializerTests/BinaryFormatterSerializerTest.cs b/test/EasyCaching.UnitTests/SerializerTests/BinaryFormatterSerializerTest.cs deleted file mode 100644 index 829b5878..00000000 --- a/test/EasyCaching.UnitTests/SerializerTests/BinaryFormatterSerializerTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EasyCaching.UnitTests -{ - using EasyCaching.Core.Serialization; - - public class BinaryFormatterSerializerTest : BaseSerializerTest - { - public BinaryFormatterSerializerTest() - { - _serializer = new DefaultBinaryFormatterSerializer(); - } - } -} \ No newline at end of file