diff --git a/src/FreeRedis.DistributedCache/FreeRedis.DistributedCache.csproj b/src/FreeRedis.DistributedCache/FreeRedis.DistributedCache.csproj index 1e6584d..db67a3c 100644 --- a/src/FreeRedis.DistributedCache/FreeRedis.DistributedCache.csproj +++ b/src/FreeRedis.DistributedCache/FreeRedis.DistributedCache.csproj @@ -4,7 +4,7 @@ FreeRedis.DistributedCache FreeRedis.DistributedCache FreeRedis.DistributedCache - 1.3.2 + 1.3.3 true https://github.com/2881099/FreeRedis 分布式缓存 FreeRedis 实现 Microsoft.Extensions.Caching diff --git a/src/FreeRedis.OpenTelemetry/FreeRedis.OpenTelemetry.csproj b/src/FreeRedis.OpenTelemetry/FreeRedis.OpenTelemetry.csproj index 5176498..c61736d 100644 --- a/src/FreeRedis.OpenTelemetry/FreeRedis.OpenTelemetry.csproj +++ b/src/FreeRedis.OpenTelemetry/FreeRedis.OpenTelemetry.csproj @@ -5,7 +5,7 @@ FreeRedis.OpenTelemetry FreeRedis.OpenTelemetry FreeRedis.OpenTelemetry - 1.3.2 + 1.3.3 true https://github.com/2881099/FreeRedis https://github.com/2881099/FreeRedis diff --git a/src/FreeRedis/FreeRedis.csproj b/src/FreeRedis/FreeRedis.csproj index da75881..d3819c1 100644 --- a/src/FreeRedis/FreeRedis.csproj +++ b/src/FreeRedis/FreeRedis.csproj @@ -5,7 +5,7 @@ FreeRedis FreeRedis FreeRedis - 1.3.2 + 1.3.3 true https://github.com/2881099/FreeRedis FreeRedis is .NET redis client, supports cluster, sentinel, master-slave, pipeline, transaction and connection pool. diff --git a/src/FreeRedis/FreeRedis.sln b/src/FreeRedis/FreeRedis.sln deleted file mode 100644 index 9f2bd1f..0000000 --- a/src/FreeRedis/FreeRedis.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeRedis", "FreeRedis.csproj", "{9C8F4D2B-B78E-43B1-BF70-1B6100C57BCA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9C8F4D2B-B78E-43B1-BF70-1B6100C57BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C8F4D2B-B78E-43B1-BF70-1B6100C57BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C8F4D2B-B78E-43B1-BF70-1B6100C57BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C8F4D2B-B78E-43B1-BF70-1B6100C57BCA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0488E242-36D8-4669-8CFA-05A7F1D9C8EF} - EndGlobalSection -EndGlobal diff --git a/src/FreeRedis/FreeRedis.xml b/src/FreeRedis/FreeRedis.xml index 03530d5..167cb77 100644 --- a/src/FreeRedis/FreeRedis.xml +++ b/src/FreeRedis/FreeRedis.xml @@ -1615,6 +1615,11 @@ CancellationToken 自动取消锁 + + + 当刷新锁时间的看门狗线程失去与Redis连接时,导致无法刷新延长锁时间时,触发此HandelLostToken Cancel + + 延长锁时间,锁在占用期内操作时返回true,若因锁超时被其他使用者占用则返回false diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs index 1a3afeb..b9ac8fc 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs @@ -77,7 +77,7 @@ public AggregationResult Execute() private List _groupByReduces = new List(); private List _sortBy = new List(); private int _sortByMax; - private List _applies = new List(); + internal List _applies = new List(); private long _limitOffset, _limitNum = 10; private string _filter; private bool _withCursor; @@ -93,7 +93,11 @@ public AggregateBuilder Verbatim(bool value = true) } public AggregateBuilder Load(params string[] fields) { - if (fields?.Any() == true) _load.AddRange(fields); + if (fields?.Any() == true) + { + _load.Clear(); + _load.AddRange(fields); + } return this; } public AggregateBuilder Timeout(long milliseconds) @@ -103,13 +107,25 @@ public AggregateBuilder Timeout(long milliseconds) } public AggregateBuilder GroupBy(params string[] properties) { - if (properties?.Any() == true) _groupBy.AddRange(properties); + if (properties?.Any() == true) + { + _groupBy.Clear(); + _groupBy.AddRange(properties); + } return this; } public AggregateBuilder GroupBy(string[] properties = null, params AggregateReduce[] reduces) { - if (properties?.Any() == true) _groupBy.AddRange(properties); - if (reduces?.Any() == true) _groupByReduces.AddRange(reduces); + if (properties?.Any() == true) + { + _groupBy.Clear(); + _groupBy.AddRange(properties); + } + if (reduces?.Any() == true) + { + _groupByReduces.Clear(); + _groupByReduces.AddRange(reduces); + } return this; } public AggregateBuilder SortBy(string property, bool desc = false) diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs index 65d675b..4271c63 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs @@ -226,6 +226,7 @@ internal protected List> ParseSelectorExpression(Ex internal protected class ParseQueryExpressionOptions { public bool IsQuoteFieldName { get; set; } = true; + public Func DiyParse { get; set; } } internal protected string ParseQueryExpression(Expression exp, ParseQueryExpressionOptions options) { @@ -239,25 +240,25 @@ object toFtObject(object param) if (param is bool || param is bool?) return (bool)param ? 1 : 0; else if (param is string str) - return string.Concat("\"", str.Replace("\\", "\\\\").Replace("\"", "\\\""), "\""); + return string.Concat("'", str.Replace("\\", "\\\\").Replace("'", "\\'"), "'"); else if (param is char chr) - return string.Concat("\"", chr.ToString().Replace("\\", "\\\\").Replace("\"", "\\\"").Replace('\0', ' '), "\""); + return string.Concat("'", chr.ToString().Replace("\\", "\\\\").Replace("'", "\\'").Replace('\0', ' '), "'"); else if (param is Enum enm) - return string.Concat("\"", enm.ToString().Replace("\\", "\\\\").Replace("\"", "\\\"").Replace(", ", ","), "\""); + return string.Concat("'", enm.ToString().Replace("\\", "\\\\").Replace("'", "\\'").Replace(", ", ","), "'"); else if (decimal.TryParse(string.Concat(param), out var trydec)) return param; else if (param is DateTime || param is DateTime?) return ToTimestamp((DateTime)param); - return string.Concat("\"", param.ToString().Replace("\\", "\\\\").Replace("\"", "\\\""), "\""); + return string.Concat("\"", param.ToString().Replace("\\", "\\\\").Replace("'", "\\'"), "'"); } string toFtTagString(string expResultStr) { if (expResultStr == null) return ""; - if (expResultStr.StartsWith("\"") && expResultStr.EndsWith("\"")) + if (expResultStr.StartsWith("'") && expResultStr.EndsWith("'")) return expResultStr.Substring(1, expResultStr.Length - 2) - .Replace("\\\"", "\"").Replace("\\\\", "\\"); + .Replace("\\'", "'").Replace("\\\\", "\\"); return expResultStr; } if (exp == null) return ""; @@ -309,10 +310,17 @@ string toFtTagString(string expResultStr) if (string.IsNullOrEmpty(memberParseResult) == false) return memberParseResult; if (memberExp.IsParameter() == false) return toFt(Expression.Lambda(exp).Compile().DynamicInvoke()); - if (_schema.KeyProperty.Name == memberExp.Member.Name) - return options.IsQuoteFieldName ? $"@__key" : "__key"; - if (_schema.FieldsMap.TryGetValue(memberExp.Member.Name, out var field)) - return options.IsQuoteFieldName ? $"@{field.FieldAttribute.FieldName}" : field.FieldAttribute.FieldName; + if (memberExp.Expression.NodeType == ExpressionType.Parameter) + { + if (_schema.KeyProperty.Name == memberExp.Member.Name) + return options.IsQuoteFieldName ? $"@__key" : "__key"; + if (_schema.FieldsMap.TryGetValue(memberExp.Member.Name, out var field)) + return options.IsQuoteFieldName ? $"@{field.FieldAttribute.FieldName}" : field.FieldAttribute.FieldName; + } + else + { + return options?.DiyParse(memberExp); + } break; string ParseMemberAccessString() @@ -378,9 +386,31 @@ string ParseCallString() var left = parseExp(callExp.Object); switch (callExp.Method.Name) { - case "StartsWith": return $"startswith({left},{parseExp(callExp.Arguments[0])})"; - case "EndsWith": return $"endswith({left},{parseExp(callExp.Arguments[0])})"; - case "Contains": return $"contains({left},{parseExp(callExp.Arguments[0])})"; + case "StartsWith": + case "EndsWith": + case "Contains": + var right = parseExp(callExp.Arguments[0]); + if (right.StartsWith("'")) + { + switch (callExp.Method.Name) + { + case "StartsWith": + case "Contains": + right = $"'*{right.Substring(1)}"; + break; + } + } + if (right.EndsWith("'")) + { + switch (callExp.Method.Name) + { + case "EndsWith": + case "Contains": + right = $"{right.Substring(0, right.Length - 1)}*'"; + break; + } + } + return $"{left}:{right}"; case "ToLower": return $"lower({left})"; case "ToUpper": return $"upper({left})"; case "Substring": @@ -661,7 +691,7 @@ public List ToList(out long total) if (prop == null) continue; if (kv.Value == null) continue; if (kv.Value is string valstr && _repository._schema.FieldsMap.TryGetValue(prop.Name, out var field) && field.FieldType == FieldType.Tag) - ttype.SetPropertyOrFieldValue(item, prop.Name, + ttype.SetPropertyOrFieldValue(item, prop.Name, field.Property.PropertyType.IsArrayOrList() ? field.Property.PropertyType.FromObject(valstr.Split(new[] { (field.FieldAttribute as FtTagFieldAttribute).Separator ?? "," }, StringSplitOptions.None)) : valstr ); @@ -797,11 +827,21 @@ public FtDocumentRepositorySearchBuilder Dialect(int value) } - public class FtDocumentRepositoryAggregateBuilder + public class FtDocumentRepositoryAggregateTuple + { + public TDocument Document { get; set; } + public TExtra Extra { get; set; } + } + public class FtDocumentRepositoryAggregateBuilder { AggregateBuilder _aggregateBuilder; - FtDocumentRepository _repository; - internal FtDocumentRepositoryAggregateBuilder(FtDocumentRepository repository, string index, string query) + FtDocumentRepository _repository; + internal FtDocumentRepositoryAggregateBuilder(FtDocumentRepository repository, AggregateBuilder aggregateBuilder) + { + _repository = repository; + _aggregateBuilder = aggregateBuilder; + } + internal FtDocumentRepositoryAggregateBuilder(FtDocumentRepository repository, string index, string query) { _repository = repository; _aggregateBuilder = new AggregateBuilder(_repository._client, index, query); @@ -838,70 +878,95 @@ internal FtDocumentRepositoryAggregateBuilder(FtDocumentRepository repository // }).ToList(); //} - public FtDocumentRepositoryAggregateBuilder Verbatim(bool value = true) + public FtDocumentRepositoryAggregateBuilder Verbatim(bool value = true) { _aggregateBuilder.Verbatim(value); return this; } - public FtDocumentRepositoryAggregateBuilder Load(Expression> selector) + public FtDocumentRepositoryAggregateBuilder Load(Expression> selector) { var fields = _repository.ParseSelectorExpression(selector.Body).Select(a => a.Value).ToArray(); if (fields.Any()) _aggregateBuilder.Load(fields); return this; } - public FtDocumentRepositoryAggregateBuilder Timeout(long milliseconds) + public FtDocumentRepositoryAggregateBuilder Timeout(long milliseconds) { _aggregateBuilder.Timeout(milliseconds); return this; } - public FtDocumentRepositoryAggregateBuilder GroupBy(params string[] properties) + public FtDocumentRepositoryAggregateBuilder GroupBy(Expression, TNewExtra>> selector) { - _aggregateBuilder.GroupBy(properties); - return this; + var fieldValues = new List>(); + var exp = selector.Body; + + if (exp.NodeType == ExpressionType.New) + { + var newExp = exp as NewExpression; + for (var a = 0; a < newExp?.Members?.Count; a++) + { + var left = newExp.Members[a].Name; + var right = _repository.ParseQueryExpression(newExp.Arguments[a], new FtDocumentRepository.ParseQueryExpressionOptions { IsQuoteFieldName = false }); + fieldValues.Add(new KeyValuePair(left, right)); + } + } + else if (exp.NodeType == ExpressionType.MemberInit) + { + var initExp = exp as MemberInitExpression; + for (var a = 0; a < initExp?.Bindings.Count; a++) + { + var initAssignExp = (initExp.Bindings[a] as MemberAssignment); + if (initAssignExp == null) continue; + var left = initAssignExp.Member.Name; + var right = _repository.ParseQueryExpression(initAssignExp.Expression, new FtDocumentRepository.ParseQueryExpressionOptions { IsQuoteFieldName = false }); + fieldValues.Add(new KeyValuePair(left, right)); + } + } + return new FtDocumentRepositoryAggregateBuilder(_repository, _aggregateBuilder); } - public FtDocumentRepositoryAggregateBuilder GroupBy(string[] properties = null, params AggregateReduce[] reduces) + public FtDocumentRepositoryAggregateBuilder GroupBy(string[] properties = null, params AggregateReduce[] reduces) { _aggregateBuilder.GroupBy(properties, reduces); return this; } - public FtDocumentRepositoryAggregateBuilder SortBy(string property, bool desc = false) + public FtDocumentRepositoryAggregateBuilder SortBy(string property, bool desc = false) { _aggregateBuilder.SortBy(property, desc); return this; } - public FtDocumentRepositoryAggregateBuilder SortBy(string[] properties, bool[] desc, int max = 0) + public FtDocumentRepositoryAggregateBuilder SortBy(string[] properties, bool[] desc, int max = 0) { _aggregateBuilder.SortBy(properties, desc, max); return this; } - public FtDocumentRepositoryAggregateBuilder Apply(Expression> selector) + public FtDocumentRepositoryAggregateBuilder Apply(Expression> selector) { var applies = _repository.ParseSelectorExpression(selector.Body, true); + _aggregateBuilder._applies.Clear(); foreach (var apply in applies) _aggregateBuilder.Apply(apply.Value, apply.Key); - return this; + return new FtDocumentRepositoryAggregateBuilder(_repository, _aggregateBuilder); } - public FtDocumentRepositoryAggregateBuilder Limit(long offset, long num) + public FtDocumentRepositoryAggregateBuilder Limit(long offset, long num) { _aggregateBuilder.Limit(offset, num); return this; } - public FtDocumentRepositoryAggregateBuilder Filter(string value) + public FtDocumentRepositoryAggregateBuilder Filter(string value) { _aggregateBuilder.Filter(value); return this; } - public FtDocumentRepositoryAggregateBuilder WithCursor(int count = -1, long maxIdle = -1) + public FtDocumentRepositoryAggregateBuilder WithCursor(int count = -1, long maxIdle = -1) { _aggregateBuilder.WithCursor(count, maxIdle); return this; } - public FtDocumentRepositoryAggregateBuilder Params(string name, string value) + public FtDocumentRepositoryAggregateBuilder Params(string name, string value) { _aggregateBuilder.Params(name, value); return this; } - public FtDocumentRepositoryAggregateBuilder Dialect(int value) + public FtDocumentRepositoryAggregateBuilder Dialect(int value) { _aggregateBuilder.Dialect(value); return this; diff --git a/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs b/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs index 6a9a54f..4c3bc54 100644 --- a/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs +++ b/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs @@ -129,7 +129,9 @@ public void FtDocumentRepository() list = repo.Search("*").Return(a => new { a.Title, a.Tags }).ToList(); list = repo.Search("*").Return(a => new { tit1 = a.Title, tgs1 = a.Tags, a.Title, a.Tags }).ToList(); - list = repo.Search(a => a.Title == "word").Filter(a => a.Views, 1, 1000).ToList(); + list = repo.Search(a => a.Title == "word" || a.Views > 100).Filter(a => a.Views, 1, 1000).ToList(); + list = repo.Search(a => a.Title.Contains("word") || a.Views > 100).Filter(a => a.Views, 1, 1000).ToList(); + list = repo.Search(a => a.Tags == "作者1").Filter(a => a.Views, 1, 1000).ToList(); list = repo.Search("word").ToList(); list = repo.Search("@title:word").ToList(); list = repo.Search("prefix*").ToList();