diff --git a/FastCache.Core/Attributes/CacheableAttribute.cs b/FastCache.Core/Attributes/CacheableAttribute.cs index f7d7be2..3302a91 100644 --- a/FastCache.Core/Attributes/CacheableAttribute.cs +++ b/FastCache.Core/Attributes/CacheableAttribute.cs @@ -12,17 +12,43 @@ namespace FastCache.Core.Attributes { + /// + /// 规则支持说明 + /// Based on the provided test cases, the `KeyGenerateHelper.GetKey` method supports the following rules for generating cache keys: + /// Supported Rules + /// 1. **Basic Property Access**: + /// - `{company:name}`: Accesses the `Name` property of the `Company` object. + /// - `{company:id}`: Accesses the `Id` property. + /// 2. **List and Array Indexing**: + /// - `{company:menus:0:openTime}`: Accesses the `openTime` of the first menu in the `Menus` list. + /// - `{company:merchants:0:merchantIds:0}`: Accesses the first `MerchantId` of the first merchant. + /// 3. **Iterating All Elements**: + /// - `{company:menus:id:all}`: Joins all `Id`s from the `Menus` list. + /// - `{company:menus:0:menuSettings:id:all}`: Joins all `Id`s from `MenuSettings` of the first menu. + /// 4. **Combining Multiple Properties**: + /// - `{company:name}:{company:id}`: Combines multiple properties into a single key. + /// - `{company:menus:id:all}:{company:id}`: Combines all menu IDs with company ID. + /// 5. **Wildcard for All Elements**: + /// - `{company:all}:{company:id}` or similar patterns can be used to indicate special handling, though specifics depend on implementation details not provided here. + /// General Structure + /// - Prefix is added at the beginning (`single:`). + /// - Patterns within curly braces are replaced with corresponding values from object properties or collections. + /// - Supports indexing and iteration over lists/arrays using specific indices or "all" for concatenation. + /// - Combinations of different patterns are supported to form complex keys. + /// Implementation Notes + /// Ensure that your method correctly interprets these patterns and handles edge cases, such as missing data or empty lists, to prevent errors like null reference exceptions. + /// public class CacheableAttribute : AbstractInterceptorAttribute { private readonly string _key; private readonly string _expression; private readonly long _expire; - + public sealed override int Order { get; set; } - + private static readonly ConcurrentDictionary TypeofTaskResultMethod = new ConcurrentDictionary(); - + private static readonly MethodInfo _taskResultMethod; static CacheableAttribute() @@ -84,9 +110,11 @@ public override async Task Invoke(AspectContext context, AspectDelegate next) ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType; - context.ReturnValue = context.IsAsync() ? TypeofTaskResultMethod.GetOrAdd(returnTypeBefore, - t => _taskResultMethod.MakeGenericMethod(returnTypeBefore)) - .Invoke(null, new [] { cacheValue.Value }) : cacheValue.Value; + context.ReturnValue = context.IsAsync() + ? TypeofTaskResultMethod.GetOrAdd(returnTypeBefore, + t => _taskResultMethod.MakeGenericMethod(returnTypeBefore)) + .Invoke(null, new[] { cacheValue.Value }) + : cacheValue.Value; return; } } @@ -103,7 +131,7 @@ public override async Task Invoke(AspectContext context, AspectDelegate next) { value = context.ReturnValue; } - + var returnType = value?.GetType(); await cacheClient.Set(key, new CacheItem diff --git a/FastCache.Core/Attributes/EvictableAttribute.cs b/FastCache.Core/Attributes/EvictableAttribute.cs index 7b2f3a6..b12a05c 100644 --- a/FastCache.Core/Attributes/EvictableAttribute.cs +++ b/FastCache.Core/Attributes/EvictableAttribute.cs @@ -7,6 +7,32 @@ namespace FastCache.Core.Attributes { + /// + /// 规则支持说明 + /// Based on the provided test cases, the `KeyGenerateHelper.GetKey` method supports the following rules for generating cache keys: + /// Supported Rules + /// 1. **Basic Property Access**: + /// - `{company:name}`: Accesses the `Name` property of the `Company` object. + /// - `{company:id}`: Accesses the `Id` property. + /// 2. **List and Array Indexing**: + /// - `{company:menus:0:openTime}`: Accesses the `openTime` of the first menu in the `Menus` list. + /// - `{company:merchants:0:merchantIds:0}`: Accesses the first `MerchantId` of the first merchant. + /// 3. **Iterating All Elements**: + /// - `{company:menus:id:all}`: Joins all `Id`s from the `Menus` list. + /// - `{company:menus:0:menuSettings:id:all}`: Joins all `Id`s from `MenuSettings` of the first menu. + /// 4. **Combining Multiple Properties**: + /// - `{company:name}:{company:id}`: Combines multiple properties into a single key. + /// - `{company:menus:id:all}:{company:id}`: Combines all menu IDs with company ID. + /// 5. **Wildcard for All Elements**: + /// - `{company:all}:{company:id}` or similar patterns can be used to indicate special handling, though specifics depend on implementation details not provided here. + /// General Structure + /// - Prefix is added at the beginning (`single:`). + /// - Patterns within curly braces are replaced with corresponding values from object properties or collections. + /// - Supports indexing and iteration over lists/arrays using specific indices or "all" for concatenation. + /// - Combinations of different patterns are supported to form complex keys. + /// Implementation Notes + /// Ensure that your method correctly interprets these patterns and handles edge cases, such as missing data or empty lists, to prevent errors like null reference exceptions. + /// public class EvictableAttribute : AbstractInterceptorAttribute { private readonly string[] _keys; diff --git a/FastCache.Core/Utils/KeyGenerateHelper.cs b/FastCache.Core/Utils/KeyGenerateHelper.cs index 05b829e..be0142c 100644 --- a/FastCache.Core/Utils/KeyGenerateHelper.cs +++ b/FastCache.Core/Utils/KeyGenerateHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,40 +9,41 @@ namespace FastCache.Core.Utils { + /// + /// 规则支持说明 + /// Based on the provided test cases, the `KeyGenerateHelper.GetKey` method supports the following rules for generating cache keys: + /// Supported Rules + /// 1. **Basic Property Access**: + /// - `{company:name}`: Accesses the `Name` property of the `Company` object. + /// - `{company:id}`: Accesses the `Id` property. + /// 2. **List and Array Indexing**: + /// - `{company:menus:0:openTime}`: Accesses the `openTime` of the first menu in the `Menus` list. + /// - `{company:merchants:0:merchantIds:0}`: Accesses the first `MerchantId` of the first merchant. + /// 3. **Iterating All Elements**: + /// - `{company:menus:id:all}`: Joins all `Id`s from the `Menus` list. + /// - `{company:menus:0:menuSettings:id:all}`: Joins all `Id`s from `MenuSettings` of the first menu. + /// 4. **Combining Multiple Properties**: + /// - `{company:name}:{company:id}`: Combines multiple properties into a single key. + /// - `{company:menus:id:all}:{company:id}`: Combines all menu IDs with company ID. + /// 5. **Wildcard for All Elements**: + /// - `{company:all}:{company:id}` or similar patterns can be used to indicate special handling, though specifics depend on implementation details not provided here. + /// General Structure + /// - Prefix is added at the beginning (`single:`). + /// - Patterns within curly braces are replaced with corresponding values from object properties or collections. + /// - Supports indexing and iteration over lists/arrays using specific indices or "all" for concatenation. + /// - Combinations of different patterns are supported to form complex keys. + /// Implementation Notes + /// Ensure that your method correctly interprets these patterns and handles edge cases, such as missing data or empty lists, to prevent errors like null reference exceptions. + /// public static class KeyGenerateHelper { public static string GetKey(string name, string originKey, IDictionary? parameters) { - var values = new ConfigurationBuilder() - .AddJsonStream( - new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(parameters)))) - .Build(); - - var reg = new Regex(@"\{([^\}]*)\}"); - var matches = reg.Matches(originKey); + var valueKey = GetKey(originKey, parameters); - foreach (Match match in matches) - { - var valueName = match.Value.Replace(@"{", "").Replace(@"}", ""); - var sections = values.GetSection(valueName).GetChildren() - .Where(x => !string.IsNullOrEmpty(x.Value)) - .ToList(); - - if (sections.Any()) - { - var valuesList = sections.Select(keyValuePair => keyValuePair.Value).ToList(); - - originKey = originKey.Replace(match.Value, string.Join(",", valuesList)); - } - else - { - originKey = originKey.Replace(match.Value, values[valueName]); - } - } - - return $"{name}:{originKey}"; + return $"{name}:{valueKey}"; } - + public static string GetKey(string originKey, IDictionary? parameters) { var values = new ConfigurationBuilder() @@ -54,20 +56,49 @@ public static string GetKey(string originKey, IDictionary? param foreach (Match match in matches) { - var valueName = match.Value.Replace(@"{", "").Replace(@"}", ""); - var sections = values.GetSection(valueName).GetChildren() - .Where(x => !string.IsNullOrEmpty(x.Value)) - .ToList(); - - if (sections.Any()) + var valueName = match.Value.Replace("{", "").Replace("}", ""); + if (valueName.Contains(":all")) { - var valuesList = sections.Select(keyValuePair => keyValuePair.Value).ToList(); + var modifiedPattern = valueName.Replace(":all", ""); + + var lastColonIndex = modifiedPattern.LastIndexOf(":", StringComparison.Ordinal); + + if (lastColonIndex == -1) + { + originKey = originKey.Replace(match.Value, ""); + continue; + } - originKey = originKey.Replace(match.Value, string.Join(",", valuesList)); + var regexPattern = "^" + Regex.Escape(modifiedPattern[..lastColonIndex]) + + @":[^:]+:" // 确保中间只允许一个非冒号的值 + + Regex.Escape(modifiedPattern[(lastColonIndex + 1)..]) + "$"; + + // 获取所有符合条件的值 + var matchValues = values.AsEnumerable().Reverse().ToList() + .Where(x => Regex.IsMatch(x.Key, regexPattern, RegexOptions.IgnoreCase)) // 匹配路径 + .Where(x => !string.IsNullOrWhiteSpace(x.Value)) // 确保值不为空白 + .Select(x => x.Value) // 提取值 + .ToList(); + + // 用匹配到的值更新 originKey + originKey = originKey.Replace(match.Value, matchValues.Any() ? string.Join(",", matchValues) : ""); } else { - originKey = originKey.Replace(match.Value, values[valueName]); + // Handle normal case + var sections = values.GetSection(valueName).GetChildren() + .Where(x => !string.IsNullOrEmpty(x.Value)) + .ToList(); + + if (sections.Any()) + { + var valuesList = sections.Select(keyValuePair => keyValuePair.Value).ToList(); + originKey = originKey.Replace(match.Value, string.Join(",", valuesList)); + } + else + { + originKey = originKey.Replace(match.Value, values[valueName]); + } } } diff --git a/FastCache.MultiSource/Attributes/MultiSourceCacheableAttribute.cs b/FastCache.MultiSource/Attributes/MultiSourceCacheableAttribute.cs index 225e6ea..5f59d16 100644 --- a/FastCache.MultiSource/Attributes/MultiSourceCacheableAttribute.cs +++ b/FastCache.MultiSource/Attributes/MultiSourceCacheableAttribute.cs @@ -13,6 +13,32 @@ namespace FastCache.MultiSource.Attributes { + /// + /// 规则支持说明 + /// Based on the provided test cases, the `KeyGenerateHelper.GetKey` method supports the following rules for generating cache keys: + /// Supported Rules + /// 1. **Basic Property Access**: + /// - `{company:name}`: Accesses the `Name` property of the `Company` object. + /// - `{company:id}`: Accesses the `Id` property. + /// 2. **List and Array Indexing**: + /// - `{company:menus:0:openTime}`: Accesses the `openTime` of the first menu in the `Menus` list. + /// - `{company:merchants:0:merchantIds:0}`: Accesses the first `MerchantId` of the first merchant. + /// 3. **Iterating All Elements**: + /// - `{company:menus:id:all}`: Joins all `Id`s from the `Menus` list. + /// - `{company:menus:0:menuSettings:id:all}`: Joins all `Id`s from `MenuSettings` of the first menu. + /// 4. **Combining Multiple Properties**: + /// - `{company:name}:{company:id}`: Combines multiple properties into a single key. + /// - `{company:menus:id:all}:{company:id}`: Combines all menu IDs with company ID. + /// 5. **Wildcard for All Elements**: + /// - `{company:all}:{company:id}` or similar patterns can be used to indicate special handling, though specifics depend on implementation details not provided here. + /// General Structure + /// - Prefix is added at the beginning (`single:`). + /// - Patterns within curly braces are replaced with corresponding values from object properties or collections. + /// - Supports indexing and iteration over lists/arrays using specific indices or "all" for concatenation. + /// - Combinations of different patterns are supported to form complex keys. + /// Implementation Notes + /// Ensure that your method correctly interprets these patterns and handles edge cases, such as missing data or empty lists, to prevent errors like null reference exceptions. + /// public class MultiSourceCacheableAttribute : AbstractInterceptorAttribute { private readonly string _key; diff --git a/FastCache.MultiSource/Attributes/MultiSourceEvictableAttribute.cs b/FastCache.MultiSource/Attributes/MultiSourceEvictableAttribute.cs index 05975f4..f541b45 100644 --- a/FastCache.MultiSource/Attributes/MultiSourceEvictableAttribute.cs +++ b/FastCache.MultiSource/Attributes/MultiSourceEvictableAttribute.cs @@ -11,6 +11,32 @@ namespace FastCache.MultiSource.Attributes { + /// + /// 规则支持说明 + /// Based on the provided test cases, the `KeyGenerateHelper.GetKey` method supports the following rules for generating cache keys: + /// Supported Rules + /// 1. **Basic Property Access**: + /// - `{company:name}`: Accesses the `Name` property of the `Company` object. + /// - `{company:id}`: Accesses the `Id` property. + /// 2. **List and Array Indexing**: + /// - `{company:menus:0:openTime}`: Accesses the `openTime` of the first menu in the `Menus` list. + /// - `{company:merchants:0:merchantIds:0}`: Accesses the first `MerchantId` of the first merchant. + /// 3. **Iterating All Elements**: + /// - `{company:menus:id:all}`: Joins all `Id`s from the `Menus` list. + /// - `{company:menus:0:menuSettings:id:all}`: Joins all `Id`s from `MenuSettings` of the first menu. + /// 4. **Combining Multiple Properties**: + /// - `{company:name}:{company:id}`: Combines multiple properties into a single key. + /// - `{company:menus:id:all}:{company:id}`: Combines all menu IDs with company ID. + /// 5. **Wildcard for All Elements**: + /// - `{company:all}:{company:id}` or similar patterns can be used to indicate special handling, though specifics depend on implementation details not provided here. + /// General Structure + /// - Prefix is added at the beginning (`single:`). + /// - Patterns within curly braces are replaced with corresponding values from object properties or collections. + /// - Supports indexing and iteration over lists/arrays using specific indices or "all" for concatenation. + /// - Combinations of different patterns are supported to form complex keys. + /// Implementation Notes + /// Ensure that your method correctly interprets these patterns and handles edge cases, such as missing data or empty lists, to prevent errors like null reference exceptions. + /// public class MultiSourceEvictableAttribute : AbstractInterceptorAttribute { private readonly string[] _keys; diff --git a/TestApi/Entity/Company.cs b/TestApi/Entity/Company.cs index adae5f6..2735f08 100644 --- a/TestApi/Entity/Company.cs +++ b/TestApi/Entity/Company.cs @@ -2,20 +2,23 @@ namespace TestApi.Entity; - public record Company : IEntity { public string Id { get; set; } public string Name { get; set; } - [NotMapped]public List? Menus { get; set; } - - [NotMapped]public List? ThirdPartyIds { get; set; } - - [NotMapped]public List? Merchants { get; set; } + [NotMapped] public List? Menus { get; set; } + + [NotMapped] public List? ThirdPartyIds { get; set; } + + [NotMapped] public List? Merchants { get; set; } } public class CompanyMenu { + public string Id { get; set; } + + public List MenuSettings { get; set; } + public DateTimeOffset openTime { get; set; } public DateTimeOffset endTime { get; set; } } @@ -23,4 +26,9 @@ public class CompanyMenu public class CompanyMerchant { public List MerchantIds { get; set; } +} + +public class MenuSetting +{ + public string Id { get; set; } } \ No newline at end of file diff --git a/UnitTests/KeyGenerateHelperTests.cs b/UnitTests/KeyGenerateHelperTests.cs index a310c25..b142b0d 100644 --- a/UnitTests/KeyGenerateHelperTests.cs +++ b/UnitTests/KeyGenerateHelperTests.cs @@ -9,22 +9,47 @@ namespace UnitTests; public class KeyGenerateHelperTests { + private const string Prefix = "single"; + private readonly string _defaultKey = $"{Prefix}:"; + + private readonly Company _companyThirdPartyIds = new() + { + Id = "3", + Name = "anson33", + ThirdPartyIds = new List() { 123, 456, 789 }, + Menus = new List() + { + new() + { + Id = "1", + MenuSettings = + [new MenuSetting { Id = "menu_setting_id_1" }, new MenuSetting() { Id = "menu_setting_id_2" }] + }, + new() + { + Id = "2", + MenuSettings = [] + } + } + }; + [Fact] public void GetCacheKey() { - const string prefix = "single"; - const string defaultKey = $"{prefix}:"; - var companyThirdPartyIds = new Company() - { - Id = "3", - Name = "anson33", - ThirdPartyIds = new List() { 123, 456, 789 } - }; + // 规则:{company:name}:{company:status} + // 输出: Prefix:anson33: + + var key1 = + KeyGenerateHelper.GetKey(Prefix, "{company:name}:{company:status}", + new Dictionary() { { "company", _companyThirdPartyIds } }); + + Assert.Equal(key1, $"{Prefix}:{_companyThirdPartyIds.Name}:"); + var arrayKey = - KeyGenerateHelper.GetKey(prefix, "{company:thirdPartyIds}", - new Dictionary() { { "company", companyThirdPartyIds } }); - - Assert.Equal(arrayKey, $"{prefix}:{string.Join(",", companyThirdPartyIds.ThirdPartyIds)}"); + KeyGenerateHelper.GetKey(Prefix, "{company:thirdPartyIds}", + new Dictionary() { { "company", _companyThirdPartyIds } }); + + Assert.Equal(arrayKey, $"{Prefix}:{string.Join(",", _companyThirdPartyIds.ThirdPartyIds!)}"); var companyMenuOpenTime = DateTimeOffset.UtcNow; var companyMenus = new Company() @@ -36,19 +61,19 @@ public void GetCacheKey() new CompanyMenu() { openTime = companyMenuOpenTime, endTime = DateTimeOffset.Now.AddHours(1) } } }; - + var companyMenusKey = - KeyGenerateHelper.GetKey(prefix, "{company:menus}", + KeyGenerateHelper.GetKey(Prefix, "{company:menus}", new Dictionary() { { "company", companyMenus } }); - - Assert.Equal(companyMenusKey, defaultKey); - + + Assert.Equal(companyMenusKey, _defaultKey); + var companyMenusFirstKey = - KeyGenerateHelper.GetKey(prefix, "{company:menus:0:openTime}", + KeyGenerateHelper.GetKey(Prefix, "{company:menus:0:openTime}", new Dictionary() { { "company", companyMenus } }); - + var milliseconds = companyMenuOpenTime.ToString("fffffff").TrimEnd('0'); - Assert.Equal(companyMenusFirstKey, $"{prefix}:{companyMenuOpenTime:yyyy-MM-ddTHH:mm:ss}.{milliseconds}+00:00"); + Assert.Equal(companyMenusFirstKey, $"{Prefix}:{companyMenuOpenTime:yyyy-MM-ddTHH:mm:ss}.{milliseconds}+00:00"); var companyMerchants = new Company() { @@ -56,21 +81,71 @@ public void GetCacheKey() Name = "company 1", Merchants = new List() { - new CompanyMerchant() { MerchantIds = new List() { "m11", "m12" } }, - new CompanyMerchant() { MerchantIds = new List(){ "m21", "m22" }} + new() { MerchantIds = ["m11", "m12"] }, + new() { MerchantIds = ["m21", "m22"] } } }; var companyMerchantsKey = - KeyGenerateHelper.GetKey(prefix, "{company:merchants}", + KeyGenerateHelper.GetKey(Prefix, "{company:merchants}", new Dictionary() { { "company", companyMerchants } }); - Assert.Equal(companyMerchantsKey, defaultKey); - + Assert.Equal(companyMerchantsKey, _defaultKey); + var companyMerchantsFirstKey = - KeyGenerateHelper.GetKey(prefix, "{company:merchants:0:merchantIds:0}", + KeyGenerateHelper.GetKey(Prefix, "{company:merchants:0:merchantIds:0}", new Dictionary() { { "company", companyMerchants } }); - Assert.Equal(companyMerchantsFirstKey, $"{prefix}:{companyMerchants.Merchants.First().MerchantIds.First()}"); + Assert.Equal(companyMerchantsFirstKey, $"{Prefix}:{companyMerchants.Merchants.First().MerchantIds.First()}"); + + // 新增规则:company:menus:id:all + // 输出: Prefix:1,2 + + var allKeysRule = + KeyGenerateHelper.GetKey(Prefix, "{company:menus:id:all}", + new Dictionary() { { "company", _companyThirdPartyIds } }); + + Assert.Equal(allKeysRule, + $"{Prefix}:{string.Join(",", _companyThirdPartyIds.Menus!.Select(x => x.Id).ToList())}"); + + // 新增规则:{company:menus:all} + // 输出: Prefix: + + var allKeysRule2 = + KeyGenerateHelper.GetKey(Prefix, "{company:menus:all}", + new Dictionary() { { "company", _companyThirdPartyIds } }); + + Assert.Equal($"{Prefix}:", allKeysRule2); + + // 新增规则:{company:menus:id:all}:{company:id} + // 输出: Prefix:1,2:3 + + var allKeysRule3 = + KeyGenerateHelper.GetKey(Prefix, "{company:menus:id:all}:{company:id}", + new Dictionary() { { "company", _companyThirdPartyIds } }); + + Assert.Equal( + $"{Prefix}:{string.Join(",", _companyThirdPartyIds.Menus!.Select(x => x.Id).ToList())}:{_companyThirdPartyIds.Id}", + allKeysRule3); + + // 新增规则:{company:menus:0:menuSettings:id:all}:{company:id} + // 输出: Prefix:menu_setting_id_1,menu_setting_id_2:3 + + var allKeysRule4 = + KeyGenerateHelper.GetKey(Prefix, "{company:menus:0:menuSettings:id:all}:{company:id}", + new Dictionary { { "company", _companyThirdPartyIds } }); + + Assert.Equal( + $"{Prefix}:{string.Join(",", _companyThirdPartyIds.Menus!.First().MenuSettings.Select(x => x.Id).ToList())}:{_companyThirdPartyIds.Id}", + allKeysRule4); + + // 新增规则:{company:all}:{company:id} + // 输出: Prefix::3 + + var allKeysRule5 = + KeyGenerateHelper.GetKey(Prefix, "{company:all}:{company:id}", + new Dictionary { { "company", _companyThirdPartyIds } }); + + Assert.Equal($"{Prefix}::{_companyThirdPartyIds.Id}", allKeysRule5); } } \ No newline at end of file