diff --git a/README.md b/README.md index ba79324..63c78e9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ FreeRedis is a redis client based on .NET, supports .NET Core 2.1+, .NET Framewo - 🎣 Support Redis Master-Slave - 📡 Support Redis Pub-Sub - 📃 Support Redis Lua Scripting -- 💻 Support Pipeline、Transaction、DelayQueue、RediSearch +- 💻 Support Pipeline, Transaction, DelayQueue, RediSearch - 🌴 Support Geo type commands (requires redis-server 3.2 and above) - 🌲 Support Streams type commands (requires redis-server 5.0 and above) - ⚡ Support Client-side-caching (requires redis-server 6.0 and above) 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/Lock.cs b/src/FreeRedis/RedisClient/Modules/Lock.cs index 8c9623c..805c997 100644 --- a/src/FreeRedis/RedisClient/Modules/Lock.cs +++ b/src/FreeRedis/RedisClient/Modules/Lock.cs @@ -5,145 +5,170 @@ namespace FreeRedis { - partial class RedisClient - { - /// - /// 开启分布式锁,若超时返回null - /// - /// 锁名称 - /// 超时(秒) - /// 自动延长锁超时时间,看门狗线程的超时时间为timeoutSeconds/2 , 在看门狗线程超时时间时自动延长锁的时间为timeoutSeconds。除非程序意外退出,否则永不超时。 - /// - public LockController Lock(string name, int timeoutSeconds, bool autoDelay = true) - { - name = $"RedisClientLock:{name}"; - var startTime = DateTime.Now; - while (DateTime.Now.Subtract(startTime).TotalSeconds < timeoutSeconds) - { - var value = Guid.NewGuid().ToString(); - if (SetNx(name, value, timeoutSeconds) == true) - { - double refreshSeconds = (double)timeoutSeconds / 2.0; - return new LockController(this, name, value, timeoutSeconds, refreshSeconds, autoDelay, CancellationToken.None); - } - Thread.CurrentThread.Join(3); - } - return null; - } - - /// - /// 开启分布式锁,若超时返回null - /// - /// 锁名称 - /// 独占锁过期时间 - /// 每隔多久自动延长一次锁,时间要比 timeoutSeconds 小,否则没有意义 - /// 等待锁释放超时时间,如果这个时间内获取不到锁,则 LockController 为 null - /// 自动延长锁超时时间,看门狗线程的超时时间为timeoutSeconds/2 , 在看门狗线程超时时间时自动延长锁的时间为timeoutSeconds。除非程序意外退出,否则永不超时。 - /// CancellationToken 自动取消锁 - /// - public LockController Lock(string name, int timeoutSeconds, int refrshTimeoutSeconds, int waitTimeoutSeconds, bool autoDelay = true, CancellationToken token = default) - { - if (refrshTimeoutSeconds == 0) throw new ArgumentException(nameof(refrshTimeoutSeconds), "刷新间隔时间不能为0"); - if (waitTimeoutSeconds == 0) waitTimeoutSeconds = refrshTimeoutSeconds; - - name = $"RedisClientLock:{name}"; - var startTime = DateTime.Now; - - // 规定时间内等待锁释放 - while (DateTime.Now.Subtract(startTime).TotalSeconds < waitTimeoutSeconds) - { - var value = Guid.NewGuid().ToString(); - if (SetNx(name, value, timeoutSeconds) == true) - { - return new LockController(this, name, value, timeoutSeconds, refrshTimeoutSeconds, autoDelay, token); - } - if (token.IsCancellationRequested) return null; - Thread.CurrentThread.Join(millisecondsTimeout: 3); - } - return null; - } - - public class LockController : IDisposable - { - - RedisClient _client; - string _name; - string _value; - int _timeoutSeconds; - Timer _autoDelayTimer; - private readonly CancellationToken _token; - internal LockController(RedisClient rds, string name, string value, int timeoutSeconds, double refreshSeconds, bool autoDelay, CancellationToken token) - { - _client = rds; - _name = name; - _value = value; - _timeoutSeconds = timeoutSeconds; - _token = token; - if (autoDelay) - { - var refreshMilli = (int)(refreshSeconds * 1000); - var timeoutMilli = timeoutSeconds * 1000; - _autoDelayTimer = new Timer(state2 => Refresh(timeoutMilli), null, refreshMilli, refreshMilli); - } - } - - /// - /// 延长锁时间,锁在占用期内操作时返回true,若因锁超时被其他使用者占用则返回false - /// - /// 延长的毫秒数 - /// 成功/失败 - public bool Delay(int milliseconds) - { - var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1]) + partial class RedisClient + { + /// + /// 开启分布式锁,若超时返回null + /// + /// 锁名称 + /// 超时(秒) + /// 自动延长锁超时时间,看门狗线程的超时时间为timeoutSeconds/2 , 在看门狗线程超时时间时自动延长锁的时间为timeoutSeconds。除非程序意外退出,否则永不超时。 + /// + public LockController Lock(string name, int timeoutSeconds, bool autoDelay = true) + { + name = $"RedisClientLock:{name}"; + var startTime = DateTime.Now; + while (DateTime.Now.Subtract(startTime).TotalSeconds < timeoutSeconds) + { + var value = Guid.NewGuid().ToString(); + if (SetNx(name, value, timeoutSeconds) == true) + { + double refreshSeconds = (double)timeoutSeconds / 2.0; + return new LockController(this, name, value, timeoutSeconds, refreshSeconds, autoDelay, CancellationToken.None); + } + Thread.CurrentThread.Join(3); + } + return null; + } + + /// + /// 开启分布式锁,若超时返回null + /// + /// 锁名称 + /// 独占锁过期时间 + /// 每隔多久自动延长一次锁,时间要比 timeoutSeconds 小,否则没有意义 + /// 等待锁释放超时时间,如果这个时间内获取不到锁,则 LockController 为 null + /// 自动延长锁超时时间,看门狗线程的超时时间为timeoutSeconds/2 , 在看门狗线程超时时间时自动延长锁的时间为timeoutSeconds。除非程序意外退出,否则永不超时。 + /// CancellationToken 自动取消锁 + /// + public LockController Lock(string name, int timeoutSeconds, int refrshTimeoutSeconds, int waitTimeoutSeconds, bool autoDelay = true, CancellationToken token = default) + { + if (refrshTimeoutSeconds == 0) throw new ArgumentException(nameof(refrshTimeoutSeconds), "刷新间隔时间不能为0"); + if (waitTimeoutSeconds == 0) waitTimeoutSeconds = refrshTimeoutSeconds; + + name = $"RedisClientLock:{name}"; + var startTime = DateTime.Now; + + // 规定时间内等待锁释放 + while (DateTime.Now.Subtract(startTime).TotalSeconds < waitTimeoutSeconds) + { + var value = Guid.NewGuid().ToString(); + if (SetNx(name, value, timeoutSeconds) == true) + { + return new LockController(this, name, value, timeoutSeconds, refrshTimeoutSeconds, autoDelay, token); + } + if (token.IsCancellationRequested) return null; + Thread.CurrentThread.Join(millisecondsTimeout: 3); + } + return null; + } + + public class LockController : IDisposable + { + + RedisClient _client; + string _name; + string _value; + int _timeoutSeconds; + Timer _autoDelayTimer; + private CancellationTokenSource _handleLostTokenSource; + private readonly CancellationToken _token; + + /// + /// 当刷新锁时间的看门狗线程失去与Redis连接时,导致无法刷新延长锁时间时,触发此HandelLostToken Cancel + /// + public CancellationToken? HandleLostToken { get; } + + internal LockController(RedisClient rds, string name, string value, int timeoutSeconds, double refreshSeconds, bool autoDelay, CancellationToken token) + { + _client = rds; + _name = name; + _value = value; + _timeoutSeconds = timeoutSeconds; + _token = token; + if (autoDelay) + { + _handleLostTokenSource = new CancellationTokenSource(); + HandleLostToken = _handleLostTokenSource.Token; + + var refreshMilli = (int)(refreshSeconds * 1000); + var timeoutMilli = timeoutSeconds * 1000; + _autoDelayTimer = new Timer(state2 => Refresh(timeoutMilli), null, refreshMilli, refreshMilli); + } + } + + /// + /// 延长锁时间,锁在占用期内操作时返回true,若因锁超时被其他使用者占用则返回false + /// + /// 延长的毫秒数 + /// 成功/失败 + public bool Delay(int milliseconds) + { + var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1]) if gva == ARGV[1] then local ttlva = redis.call('PTTL', KEYS[1]) redis.call('PEXPIRE', KEYS[1], ARGV[2] + ttlva) return 1 end return 0", new[] { _name }, _value, milliseconds)?.ToString() == "1"; - if (ret == false) _autoDelayTimer?.Dispose(); //未知情况,关闭定时器 - return ret; - } - - /// - /// 刷新锁时间,把key的ttl重新设置为milliseconds,锁在占用期内操作时返回true,若因锁超时被其他使用者占用则返回false - /// - /// 刷新的毫秒数 - /// 成功/失败 - public bool Refresh(int milliseconds) - { - var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1]) + if (ret == false) _autoDelayTimer?.Dispose(); //未知情况,关闭定时器 + return ret; + } + + /// + /// 刷新锁时间,把key的ttl重新设置为milliseconds,锁在占用期内操作时返回true,若因锁超时被其他使用者占用则返回false + /// + /// 刷新的毫秒数 + /// 成功/失败 + public bool Refresh(int milliseconds) + { + if (_token.IsCancellationRequested) + { + _autoDelayTimer?.Dispose(); + } + + try + { + var ret = _client.Eval(@"local gva = redis.call('GET', KEYS[1]) if gva == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end return 0", new[] { _name }, _value, milliseconds)?.ToString() == "1"; - if (ret == false) _autoDelayTimer?.Dispose(); //未知情况,关闭定时器 - - if (_token.IsCancellationRequested) - { - _autoDelayTimer?.Dispose(); - } - return ret; - } - - /// - /// 释放分布式锁 - /// - /// 成功/失败 - public bool Unlock() - { - _autoDelayTimer?.Dispose(); - return _client.Eval(@"local gva = redis.call('GET', KEYS[1]) + if (ret == false) + { + _handleLostTokenSource?.Cancel(); + _autoDelayTimer?.Dispose(); //未知情况,关闭定时器 + } + + return ret; + } + catch + { + _handleLostTokenSource?.Cancel(); + _autoDelayTimer?.Dispose(); //未知情况,关闭定时器 + return false;//这里必须要吞掉异常,否则会导致整个程序崩溃,因为Timer的异常没有地方去处理 + } + } + + /// + /// 释放分布式锁 + /// + /// 成功/失败 + public bool Unlock() + { + _handleLostTokenSource?.Dispose(); + _autoDelayTimer?.Dispose(); + return _client.Eval(@"local gva = redis.call('GET', KEYS[1]) if gva == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 end return 0", new[] { _name }, _value)?.ToString() == "1"; - } + } - public void Dispose() => this.Unlock(); - } - } + public void Dispose() => this.Unlock(); + } + } } diff --git a/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/AggregateBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/AggregateBuilder.cs index df19e25..39e4418 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/AggregateBuilder.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/AggregateBuilder.cs @@ -87,7 +87,7 @@ CommandPacket GetCommandPacket() 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; @@ -103,7 +103,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) @@ -113,13 +117,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/RediSearchBuilder/FtDocumentRepository.cs b/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/FtDocumentRepository.cs index ad424e6..b7df736 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/FtDocumentRepository.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearchBuilder/FtDocumentRepository.cs @@ -278,6 +278,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) { @@ -291,25 +292,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 ""; @@ -361,10 +362,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() @@ -430,9 +438,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": @@ -862,11 +892,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); @@ -903,70 +943,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();