Browse Source

feat: Optimize Query Translator

pull/1552/head
colin 1 week ago
parent
commit
47fc782fac
  1. 31
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs
  2. 98
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Collection.cs
  3. 25
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Enum.cs
  4. 97
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Enumerable.cs
  5. 114
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Field.cs
  6. 317
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs
  7. 536
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
  8. 2
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IExpressionQueryService.cs
  9. 8
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/README.md
  10. 122
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestModule.cs
  11. 5
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/DefaultExpressionQueryService_Tests.cs
  12. 169
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryService_Tests.cs
  13. 103
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs

31
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs

@ -46,6 +46,7 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
int skipCount = 0,
Fields? sourceExcludes = null,
Fields? sourceIncludes = null,
object[]? beginMarker = null,
CancellationToken cancellationToken = default) where TDocument : class
{
var client = ClientFactory.Create();
@ -79,7 +80,8 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
maxResultCount,
skipCount,
sourceExcludes,
sourceIncludes,
sourceIncludes,
beginMarker,
cancellationToken)
: await SearchFromSize<TDocument>(
client,
@ -137,19 +139,28 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
string indexName,
Query query,
SortOptions[] sorts,
int maxResultCount = 50,
int skipCount = 0,
int maxResultCount,
int skipCount,
Fields? sourceExcludes = null,
Fields? sourceIncludes = null,
object[]? beginMarker = null,
CancellationToken cancellationToken = default)
{
var searchAfter = await GetSearchAfterValue<TDocument>(
client,
indexName,
query,
sorts,
skipCount,
cancellationToken);
List<FieldValue>? searchAfter = null;
if (beginMarker != null)
{
searchAfter = beginMarker.Select(FieldValue.FromValue).ToList();
}
else
{
searchAfter = await GetSearchAfterValue<TDocument>(
client,
indexName,
query,
sorts,
skipCount,
cancellationToken);
}
if (searchAfter == null || !searchAfter.Any())
{

98
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Collection.cs

@ -0,0 +1,98 @@
using Elastic.Clients.Elasticsearch.QueryDsl;
using System;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public partial class ExpressionQueryTranslator
{
/// <summary>
/// 翻译集合 Contains
/// </summary>
private Query TranslateCollectionContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
// list.Contains(value) 或 hashSet.Contains(value)
// 有两种情况:
// 1. 外部集合.Contains(字段) - 例如:new[]{"a","b"}.Contains(x.xxx)
// 2. 字段.Contains(值) - 例如:x.Y.Contains("B")
Expression collectionExpr = node.Object!;
Expression valueExpr = node.Arguments[0];
// 判断哪个是字段,哪个是值
var collectionIsField = IsFieldLike(collectionExpr);
var valueIsField = IsFieldLike(valueExpr);
if (collectionIsField && !valueIsField)
{
// 情况 2:字段.Contains(值)
// 这通常用于集合字段,如 x.Tags.Contains("B")
var field = ResolveField(collectionExpr, prefix, mappingInfo);
var value = Evaluate(valueExpr);
if (value == null)
{
return new MatchNoneQuery();
}
// 对于集合字段,使用 Terms 查询或 Term 查询
// 如果字段是数组或集合类型,单个值的 Contains 实际上就是 Term 查询
return BuildEquality(field, value);
}
else if (!collectionIsField && valueIsField)
{
// 情况 1:外部集合.Contains(字段)
var field = ResolveField(valueExpr, prefix, mappingInfo);
// 尝试获取集合的值
var collectionValue = Evaluate(collectionExpr);
if (collectionValue is IEnumerable enumerable && collectionValue is not string)
{
var values = enumerable.Cast<object>().Select(NormalizeValue).ToList();
if (values.Count == 0)
{
return new MatchNoneQuery();
}
if (values.Count == 1)
{
// 如果集合只有一个值,使用 TermQuery
return BuildEquality(field, values[0]);
}
// 多个值使用 TermsQuery
return new TermsQuery { Field = field.Path, Terms = new TermsQueryField(values) };
}
// 如果集合值无法获取,使用默认处理
var defaultValue = Evaluate(valueExpr);
return BuildEquality(field, defaultValue!);
}
else if (collectionIsField && valueIsField)
{
// 两个都是字段,这种情况较少见
throw new NotSupportedException($"Unsupported Contains with two field expressions: {node}");
}
else
{
// 两个都不是字段,尝试直接求值
var collectionValue = Evaluate(collectionExpr);
var value = Evaluate(valueExpr);
if (collectionValue is IEnumerable enumerable && collectionValue is not string)
{
if (enumerable.Cast<object>().Contains(value))
{
return new MatchAllQuery();
}
else
{
return new MatchNoneQuery();
}
}
throw new NotSupportedException($"Unsupported Contains expression: {node}");
}
}
}

25
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Enum.cs

@ -0,0 +1,25 @@
using Elastic.Clients.Elasticsearch.QueryDsl;
using System;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public partial class ExpressionQueryTranslator
{
/// <summary>
/// 翻译 Enum.HasFlag
/// </summary>
private Query TranslateEnumHasFlag(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var field = ResolveField(node.Object!, prefix, mappingInfo);
var flag = Evaluate(node.Arguments[0]);
if (flag == null)
{
throw new NotSupportedException("Cannot use null flag in Enum.HasFlag");
}
var flagValue = Convert.ToInt64(flag);
return new TermQuery { Field = field.Path, Value = flagValue };
}
}

97
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Enumerable.cs

@ -0,0 +1,97 @@
using Elastic.Clients.Elasticsearch.QueryDsl;
using System;
using System.Linq;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public partial class ExpressionQueryTranslator
{
/// <summary>
/// 翻译 Enumerable 方法
/// </summary>
private Query TranslateEnumerableMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
switch (node.Method.Name)
{
case nameof(Enumerable.Any):
return TranslateEnumerableAny(node, prefix, mappingInfo);
case nameof(Enumerable.Contains):
return TranslateEnumerableContains(node, prefix, mappingInfo);
case nameof(Enumerable.All):
return TranslateEnumerableAll(node, prefix, mappingInfo);
default:
throw new NotSupportedException($"Unsupported Enumerable method {node.Method.Name}");
}
}
/// <summary>
/// 翻译 Enumerable.Any
/// </summary>
private Query TranslateEnumerableAny(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo);
Query inner;
if (node.Arguments.Count == 1)
{
// .Any() 检查集合是否存在
inner = new ExistsQuery { Field = collectionField.Path };
}
else
{
// .Any(predicate)
var predicate = UnwrapLambda(node.Arguments[1]);
inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo);
}
var shouldUseNested = collectionField.IsNested || (mappingInfo?.IsNested(collectionField.Path) ?? false);
return shouldUseNested
? new NestedQuery(collectionField.Path, inner)
: inner;
}
/// <summary>
/// 翻译 Enumerable.Contains
/// </summary>
private Query TranslateEnumerableContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
Expression collectionExpr;
Expression valueExpr;
if (node.Object != null)
{
// list.Contains(value)
collectionExpr = node.Object;
valueExpr = node.Arguments[0];
}
else
{
// Enumerable.Contains(list, value)
collectionExpr = node.Arguments[0];
valueExpr = node.Arguments[1];
}
var field = ResolveField(collectionExpr, prefix, mappingInfo);
var value = Evaluate(valueExpr);
return BuildTermsQuery(field, value!);
}
/// <summary>
/// 翻译 Enumerable.All
/// </summary>
private Query TranslateEnumerableAll(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo);
var predicate = UnwrapLambda(node.Arguments[1]);
var inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo);
return new NestedQuery(collectionField.Path, inner);
}
}

114
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Field.cs

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json.Serialization;
namespace LINGYUN.Abp.Elasticsearch;
public partial class ExpressionQueryTranslator
{
/// <summary>
/// 解析字段
/// </summary>
protected virtual FieldInfo ResolveField(Expression expression, string? prefix, IndexMappingInfo? mappingInfo)
{
var current = expression;
while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary)
{
current = unary.Operand;
}
var names = new Stack<string>();
Type? leafType = null;
string? leafName = null;
// 收集成员路径
while (current is MemberExpression member)
{
leafName ??= member.Member.Name;
leafType ??= GetMemberType(member.Member);
names.Push(ResolveFieldName(member.Member));
current = member.Expression!;
}
if (current is not ParameterExpression && current is not ConstantExpression)
{
throw new NotSupportedException($"Unable to parse as field path: {expression}");
}
var path = string.Join(".", names);
// 应用前缀
if (!string.IsNullOrEmpty(prefix) && path.Length > 0)
{
path = prefix + "." + path;
}
else if (path.Length == 0)
{
path = prefix ?? string.Empty;
}
// 获取字段映射信息
var finalMapping = mappingInfo?.GetField(path);
// 如果是 text 类型且有 keyword 子字段,自动使用 .keyword
if (finalMapping?.IsText == true && finalMapping.Properties?.ContainsKey("keyword") == true)
{
path = $"{path}.keyword";
finalMapping = mappingInfo?.GetField(path);
}
// 如果 leafType 为 null,使用 expression.Type
var type = leafType ?? expression.Type;
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
return new FieldInfo(
path,
underlyingType,
leafName ?? string.Empty,
finalMapping?.IsKeyword ?? false,
finalMapping?.IsText ?? false,
finalMapping?.IsWildcard ?? false,
finalMapping?.IsNested ?? false || (mappingInfo?.IsNested(path) ?? false),
finalMapping?.IsDate ?? false,
finalMapping?.IsNumeric ?? false,
finalMapping?.IsBoolean ?? false,
finalMapping?.IsRange ?? false,
finalMapping?.Format,
finalMapping?.HasMultiFields ?? false
);
}
/// <summary>
/// 获取成员的实际类型
/// </summary>
private static Type GetMemberType(MemberInfo member)
{
return member switch
{
System.Reflection.FieldInfo field => field.FieldType,
PropertyInfo property => property.PropertyType,
MethodInfo method => method.ReturnType,
_ => typeof(object)
};
}
/// <summary>
/// 解析字段名称
/// </summary>
private static string ResolveFieldName(MemberInfo member)
{
// 检查 JsonPropertyName 属性
if (member is PropertyInfo property)
{
var jsonName = property.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonName != null && !string.IsNullOrWhiteSpace(jsonName.Name))
{
return jsonName.Name;
}
}
return member.Name;
}
}

317
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs

@ -0,0 +1,317 @@
using Elastic.Clients.Elasticsearch.QueryDsl;
using System;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public partial class ExpressionQueryTranslator
{
/// <summary>
/// 翻译字符串方法
/// </summary>
private Query TranslateStringMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
return node.Method.Name switch
{
// 实例方法
nameof(string.Contains) => TranslateStringContains(node, prefix, mappingInfo),
nameof(string.StartsWith) => TranslateStringStartsWith(node, prefix, mappingInfo),
nameof(string.EndsWith) => TranslateStringEndsWith(node, prefix, mappingInfo),
nameof(string.Equals) => TranslateStringEquals(node, prefix, mappingInfo),
nameof(string.CompareTo) => TranslateStringCompareTo(node, prefix, mappingInfo),
nameof(string.IndexOf) => TranslateStringIndexOf(node, prefix, mappingInfo),
// 静态方法
nameof(string.IsNullOrEmpty) => TranslateStringIsNullOrEmpty(node, prefix, mappingInfo),
nameof(string.IsNullOrWhiteSpace) => TranslateStringIsNullOrWhiteSpace(node, prefix, mappingInfo),
// 其他方法不支持
_ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"),
};
}
/// <summary>
/// 翻译 Contains
/// </summary>
private Query TranslateStringContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
var fieldMapping = mappingInfo?.GetField(field.Path);
return TranslateStringContains(field, fieldMapping, value);
}
/// <summary>
/// 翻译 StartsWith
/// </summary>
private Query TranslateStringStartsWith(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
var fieldMapping = mappingInfo?.GetField(field.Path);
return TranslateStartsWith(field, fieldMapping, value);
}
/// <summary>
/// 翻译 EndsWith
/// </summary>
private Query TranslateStringEndsWith(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
var fieldMapping = mappingInfo?.GetField(field.Path);
return TranslateEndsWith(field, fieldMapping, value);
}
/// <summary>
/// 翻译 StartsWith
/// </summary>
private Query TranslateStartsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
var pattern = EscapeWildcard(value) + "*";
// Wildcard 类型
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery { Field = field.Path, Value = pattern };
}
// Keyword 类型或 Text 有 keyword 子字段
if (field.IsKeyword || fieldMapping?.IsKeyword == true ||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true))
{
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true
? $"{field.Path}.keyword"
: field.Path;
return new WildcardQuery { Field = fieldPath, Value = pattern };
}
// Text 类型无 keyword 子字段
if (field.IsText || fieldMapping?.IsText == true)
{
return new MatchPhrasePrefixQuery
{
Field = field.Path,
Query = value
};
}
return new WildcardQuery { Field = field.Path, Value = pattern };
}
/// <summary>
/// 翻译 EndsWith
/// </summary>
private Query TranslateEndsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
var pattern = "*" + EscapeWildcard(value);
// Wildcard 类型
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery { Field = field.Path, Value = pattern };
}
// Keyword 类型或 Text 有 keyword 子字段
if (field.IsKeyword || fieldMapping?.IsKeyword == true ||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true))
{
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true
? $"{field.Path}.keyword"
: field.Path;
return new WildcardQuery { Field = fieldPath, Value = pattern };
}
// Text 类型无 keyword 子字段
if (field.IsText || fieldMapping?.IsText == true)
{
return new MatchPhraseQuery
{
Field = field.Path,
Query = value
};
}
return new WildcardQuery { Field = field.Path, Value = pattern };
}
/// <summary>
/// 翻译 string.Equals
/// </summary>
private Query TranslateStringEquals(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
if (string.IsNullOrEmpty(value))
{
return new BoolQuery { MustNot = new Query[] { new ExistsQuery { Field = field.Path } } };
}
return BuildEquality(field, value);
}
/// <summary>
/// 翻译 string.CompareTo
/// </summary>
private Query TranslateStringCompareTo(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
// CompareTo 通常用于比较,这里简化为相等比较
// 如果需要更复杂的比较逻辑,可以在这里扩展
return BuildEquality(field, value);
}
/// <summary>
/// 翻译 string.IndexOf
/// </summary>
private Query TranslateStringIndexOf(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo);
var fieldMapping = mappingInfo?.GetField(field.Path);
// IndexOf >= 0 等价于 Contains
return TranslateStringContains(field, fieldMapping, value);
}
/// <summary>
/// 翻译 Contains
/// </summary>
private Query TranslateStringContains(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
// 1. Wildcard 类型 - 直接使用通配符查询(最优)
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 2. Keyword 类型 - 使用通配符查询
if (field.IsKeyword || fieldMapping?.IsKeyword == true)
{
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 3. Text 类型
if (field.IsText || fieldMapping?.IsText == true)
{
// 3.1 如果有 keyword 子字段,使用 .keyword 进行通配符查询
if (fieldMapping?.Properties?.ContainsKey("keyword") == true)
{
return new WildcardQuery
{
Field = $"{field.Path}.keyword",
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 3.2 没有 keyword 子字段,使用 MatchPhrase 进行全文搜索
// 注意:这不是精确的 Contains,而是分词后的短语匹配
return new MatchPhraseQuery
{
Field = field.Path,
Query = value
};
}
// 4. 默认 - 尝试使用通配符
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
/// <summary>
/// 翻译 string.IsNullOrEmpty
/// </summary>
private Query TranslateStringIsNullOrEmpty(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var field = ResolveField(node.Arguments[0], prefix, mappingInfo);
// field == null || field == ""
return new BoolQuery
{
Should = new Query[]
{
// null 或不存在
new BoolQuery
{
MustNot = new Query[] { new ExistsQuery { Field = field.Path } }
},
// 空字符串
new TermQuery
{
Field = field.Path,
Value = string.Empty
}
},
MinimumShouldMatch = 1
};
}
/// <summary>
/// 翻译 string.IsNullOrWhiteSpace
/// </summary>
private Query TranslateStringIsNullOrWhiteSpace(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var field = ResolveField(node.Arguments[0], prefix, mappingInfo);
// field == null || field == "" || Regex.IsMatch(field, @"^\s*$")
return new BoolQuery
{
Should = new Query[]
{
// null 或不存在
new BoolQuery
{
MustNot = new Query[] { new ExistsQuery { Field = field.Path } }
},
// 空字符串
new TermQuery
{
Field = field.Path,
Value = string.Empty
},
// 使用正则表达式匹配只有空白字符的字符串
new RegexpQuery
{
Field = field.Path,
Value = @"^\s*$"
}
},
MinimumShouldMatch = 1
};
}
/// <summary>
/// 翻译字符串实例方法(获取字段和值)
/// </summary>
private (FieldInfo Field, string Value) GetStringMethodOperands(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
Expression fieldExpression;
Expression valueExpression;
if (node.Object != null)
{
// 实例方法:obj.Method(value)
fieldExpression = node.Object;
valueExpression = node.Arguments[0];
}
else
{
// 静态方法:string.Method(field, value)
fieldExpression = node.Arguments[0];
valueExpression = node.Arguments.Count > 1 ? node.Arguments[1] : node.Arguments[0];
}
var field = ResolveField(fieldExpression, prefix, mappingInfo);
var value = Evaluate(valueExpression)?.ToString() ?? string.Empty;
return (field, value);
}
}

536
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs

@ -16,7 +16,7 @@ namespace LINGYUN.Abp.Elasticsearch;
/// <summary>
/// 表达式查询转换器 - 将 LINQ 表达式转换为 Elasticsearch Query
/// </summary>
public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonDependency
public partial class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonDependency
{
private readonly IIndexMappingProvider _indexMappingProvider;
@ -484,14 +484,8 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
return TranslateEnumerableMethod(node, prefix, mappingInfo);
}
// string.Equals 需放在其他方法前
if (node.Method.Name == nameof(string.Equals))
{
return TranslateStringEquals(node, prefix, mappingInfo);
}
// 字符串方法
if (node.Method.DeclaringType == typeof(string) && node.Object != null)
if (node.Method.DeclaringType == typeof(string))
{
return TranslateStringMethod(node, prefix, mappingInfo);
}
@ -512,341 +506,6 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
$"Unsupported method invocation {node.Method.DeclaringType?.Name}.{node.Method.Name}");
}
/// <summary>
/// 翻译 Enumerable 方法
/// </summary>
private Query TranslateEnumerableMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
switch (node.Method.Name)
{
case nameof(Enumerable.Any):
return TranslateEnumerableAny(node, prefix, mappingInfo);
case nameof(Enumerable.Contains):
return TranslateEnumerableContains(node, prefix, mappingInfo);
case nameof(Enumerable.All):
return TranslateEnumerableAll(node, prefix, mappingInfo);
default:
throw new NotSupportedException($"Unsupported Enumerable method {node.Method.Name}");
}
}
/// <summary>
/// 翻译 Enumerable.Any
/// </summary>
private Query TranslateEnumerableAny(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo);
Query inner;
if (node.Arguments.Count == 1)
{
// .Any() 检查集合是否存在
inner = new ExistsQuery { Field = collectionField.Path };
}
else
{
// .Any(predicate)
var predicate = UnwrapLambda(node.Arguments[1]);
inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo);
}
var shouldUseNested = collectionField.IsNested || (mappingInfo?.IsNested(collectionField.Path) ?? false);
return shouldUseNested
? new NestedQuery(collectionField.Path, inner)
: inner;
}
/// <summary>
/// 翻译 Enumerable.Contains
/// </summary>
private Query TranslateEnumerableContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
Expression collectionExpr;
Expression valueExpr;
if (node.Object != null)
{
// list.Contains(value)
collectionExpr = node.Object;
valueExpr = node.Arguments[0];
}
else
{
// Enumerable.Contains(list, value)
collectionExpr = node.Arguments[0];
valueExpr = node.Arguments[1];
}
var field = ResolveField(collectionExpr, prefix, mappingInfo);
var value = Evaluate(valueExpr);
return BuildTermsQuery(field, value!);
}
/// <summary>
/// 翻译 Enumerable.All
/// </summary>
private Query TranslateEnumerableAll(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo);
var predicate = UnwrapLambda(node.Arguments[1]);
var inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo);
return new NestedQuery(collectionField.Path, inner);
}
/// <summary>
/// 翻译字符串方法
/// </summary>
private Query TranslateStringMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var field = ResolveField(node.Object!, prefix, mappingInfo);
var value = (string)Evaluate(node.Arguments[0])!;
var fieldMapping = mappingInfo?.GetField(field.Path);
var escapedValue = EscapeWildcard(value);
return node.Method.Name switch
{
nameof(string.Contains) => TranslateContains(field, fieldMapping, value),
nameof(string.StartsWith) => TranslateStartsWith(field, fieldMapping, value),
nameof(string.EndsWith) => TranslateEndsWith(field, fieldMapping, value),
_ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"),
};
}
/// <summary>
/// 翻译 Contains
/// </summary>
private Query TranslateContains(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
// 1. Wildcard 类型 - 直接使用通配符查询(最优)
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 2. Keyword 类型 - 使用通配符查询
if (field.IsKeyword || fieldMapping?.IsKeyword == true)
{
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 3. Text 类型
if (field.IsText || fieldMapping?.IsText == true)
{
// 3.1 如果有 keyword 子字段,使用 .keyword 进行通配符查询
if (fieldMapping?.Properties?.ContainsKey("keyword") == true)
{
return new WildcardQuery
{
Field = $"{field.Path}.keyword",
Value = "*" + EscapeWildcard(value) + "*"
};
}
// 3.2 没有 keyword 子字段,使用 MatchPhrase 进行全文搜索
// 注意:这不是精确的 Contains,而是分词后的短语匹配
return new MatchPhraseQuery
{
Field = field.Path,
Query = value
};
}
// 4. 默认 - 尝试使用通配符
return new WildcardQuery
{
Field = field.Path,
Value = "*" + EscapeWildcard(value) + "*"
};
}
/// <summary>
/// 翻译 StartsWith
/// </summary>
private Query TranslateStartsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
var pattern = EscapeWildcard(value) + "*";
// Wildcard 类型
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery { Field = field.Path, Value = pattern };
}
// Keyword 类型或 Text 有 keyword 子字段
if (field.IsKeyword || fieldMapping?.IsKeyword == true ||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true))
{
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true
? $"{field.Path}.keyword"
: field.Path;
return new WildcardQuery { Field = fieldPath, Value = pattern };
}
// Text 类型无 keyword 子字段
if (field.IsText || fieldMapping?.IsText == true)
{
return new MatchPhrasePrefixQuery
{
Field = field.Path,
Query = value
};
}
return new WildcardQuery { Field = field.Path, Value = pattern };
}
/// <summary>
/// 翻译 EndsWith
/// </summary>
private Query TranslateEndsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value)
{
var pattern = "*" + EscapeWildcard(value);
// Wildcard 类型
if (field.IsWildcard || fieldMapping?.IsWildcard == true)
{
return new WildcardQuery { Field = field.Path, Value = pattern };
}
// Keyword 类型或 Text 有 keyword 子字段
if (field.IsKeyword || fieldMapping?.IsKeyword == true ||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true))
{
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true
? $"{field.Path}.keyword"
: field.Path;
return new WildcardQuery { Field = fieldPath, Value = pattern };
}
// Text 类型无 keyword 子字段
if (field.IsText || fieldMapping?.IsText == true)
{
return new MatchPhraseQuery
{
Field = field.Path,
Query = value
};
}
return new WildcardQuery { Field = field.Path, Value = pattern };
}
/// <summary>
/// 翻译 string.Equals
/// </summary>
private Query TranslateStringEquals(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
Expression fieldExpression;
Expression valueExpression;
if (node.Object != null)
{
fieldExpression = node.Object;
valueExpression = node.Arguments[0];
}
else
{
fieldExpression = node.Arguments[0];
valueExpression = node.Arguments[1];
}
var field = ResolveField(fieldExpression, prefix, mappingInfo);
var value = Evaluate(valueExpression);
if (value == null)
{
return new BoolQuery { MustNot = new Query[] { new ExistsQuery { Field = field.Path } } };
}
return BuildEquality(field, value);
}
/// <summary>
/// 翻译 Enum.HasFlag
/// </summary>
private Query TranslateEnumHasFlag(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
var field = ResolveField(node.Object!, prefix, mappingInfo);
var flag = Evaluate(node.Arguments[0]);
if (flag == null)
{
throw new NotSupportedException("Cannot use null flag in Enum.HasFlag");
}
var flagValue = Convert.ToInt64(flag);
return new TermQuery { Field = field.Path, Value = flagValue };
}
/// <summary>
/// 翻译集合 Contains
/// </summary>
private Query TranslateCollectionContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
{
// list.Contains(value) 或 hashSet.Contains(value)
// node.Object = 集合实例 (可能是一个变量或常量)
// node.Arguments[0] = value (要检查的值)
// 尝试获取集合的值
var collectionValue = Evaluate(node.Object!);
// 如果集合是常量且可枚举,构建 TermsQuery
if (collectionValue is IEnumerable enumerable && collectionValue is not string)
{
var values = enumerable.Cast<object>().Select(NormalizeValue).ToList();
if (values.Count == 0)
{
return new MatchNoneQuery();
}
if (values.Count == 1)
{
// 如果集合只有一个值,使用 TermQuery
return BuildEquality(ResolveField(node.Arguments[0], prefix, mappingInfo), values[0]);
}
// 多个值使用 TermsQuery
var field = ResolveField(node.Arguments[0], prefix, mappingInfo);
return new TermsQuery { Field = field.Path, Terms = new TermsQueryField(values) };
}
// 如果值是可枚举集合
var value = Evaluate(node.Arguments[0]);
if (value is IEnumerable enumerableValue && value is not string)
{
var values = enumerableValue.Cast<object>().Select(NormalizeValue).ToList();
if (values.Count == 0)
{
return new MatchNoneQuery();
}
if (values.Count == 1)
{
return BuildEquality(ResolveField(node.Object!, prefix, mappingInfo), values[0]);
}
var field = ResolveField(node.Object!, prefix, mappingInfo);
return new TermsQuery { Field = field.Path, Terms = new TermsQueryField(values) };
}
// 默认:检查字段是否在集合中
// 这种情况下,我们使用 TermsQuery 但需要从外部获取集合值
// 由于无法在编译时确定,使用 TermQuery 进行单值匹配
var defaultField = ResolveField(node.Arguments[0], prefix, mappingInfo);
return BuildEquality(defaultField, value!);
}
#endregion
#region 查询构建
@ -1057,27 +716,79 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
/// </summary>
private static object? Evaluate(Expression expression)
{
// 如果是常量表达式,直接返回值
if (expression is ConstantExpression constant)
{
return constant.Value;
}
// 如果是参数表达式,无法求值
if (expression is ParameterExpression)
{
throw new InvalidOperationException($"Cannot evaluate parameter expression: {expression}");
}
// 处理成员访问(捕获外部变量)
if (expression is MemberExpression memberExpr)
{
object? obj = null;
if (memberExpr.Expression != null)
{
obj = Evaluate(memberExpr.Expression);
}
// 检查是否是字段或属性访问
if (memberExpr.Member is System.Reflection.FieldInfo fieldInfo)
{
return fieldInfo.GetValue(obj);
// 如果是静态字段
if (fieldInfo.IsStatic)
{
return fieldInfo.GetValue(null);
}
// 如果是实例字段,需要先求值实例
if (memberExpr.Expression != null)
{
var obj = Evaluate(memberExpr.Expression);
if (obj != null)
{
return fieldInfo.GetValue(obj);
}
}
// 如果无法求值,尝试编译整个表达式
try
{
return Expression.Lambda(memberExpr).Compile().DynamicInvoke();
}
catch
{
throw new InvalidOperationException($"Cannot evaluate member expression: {memberExpr}");
}
}
if (memberExpr.Member is PropertyInfo propertyInfo)
{
return propertyInfo.GetValue(obj);
// 如果是静态属性
var getMethod = propertyInfo.GetGetMethod();
if (getMethod != null && getMethod.IsStatic)
{
return propertyInfo.GetValue(null);
}
// 如果是实例属性,需要先求值实例
if (memberExpr.Expression != null)
{
var obj = Evaluate(memberExpr.Expression);
if (obj != null)
{
return propertyInfo.GetValue(obj);
}
}
// 如果无法求值,尝试编译整个表达式
try
{
return Expression.Lambda(memberExpr).Compile().DynamicInvoke();
}
catch
{
throw new InvalidOperationException($"Cannot evaluate member expression: {memberExpr}");
}
}
}
@ -1097,11 +808,18 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
{
try
{
return Expression.Lambda(methodCall).Compile().DynamicInvoke();
// 尝试编译并执行
var lambda = Expression.Lambda(methodCall);
return lambda.Compile().DynamicInvoke();
}
catch
catch (InvalidOperationException)
{
return null;
// 如果包含参数引用,无法编译
throw;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Cannot evaluate method call: {methodCall}", ex);
}
}
@ -1112,6 +830,8 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
return values;
}
// 默认尝试编译执行
return Expression.Lambda(expression).Compile().DynamicInvoke();
}
@ -1241,112 +961,4 @@ public class ExpressionQueryTranslator : IExpressionQueryTranslator, ISingletonD
}
#endregion
#region 字段解析
/// <summary>
/// 解析字段
/// </summary>
protected virtual FieldInfo ResolveField(Expression expression, string? prefix, IndexMappingInfo? mappingInfo)
{
var current = expression;
while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary)
{
current = unary.Operand;
}
var names = new Stack<string>();
Type? leafType = null;
string? leafName = null;
// 收集成员路径
while (current is MemberExpression member)
{
leafName ??= member.Member.Name;
leafType ??= GetMemberType(member.Member);
names.Push(ResolveFieldName(member.Member));
current = member.Expression!;
}
if (current is not ParameterExpression && current is not ConstantExpression)
{
throw new NotSupportedException($"Unable to parse as field path: {expression}");
}
var path = string.Join(".", names);
// 应用前缀
if (!string.IsNullOrEmpty(prefix) && path.Length > 0)
{
path = prefix + "." + path;
}
else if (path.Length == 0)
{
path = prefix ?? string.Empty;
}
// 获取字段映射信息
var finalMapping = mappingInfo?.GetField(path);
// 如果是 text 类型且有 keyword 子字段,自动使用 .keyword
if (finalMapping?.IsText == true && finalMapping.Properties?.ContainsKey("keyword") == true)
{
path = $"{path}.keyword";
finalMapping = mappingInfo?.GetField(path);
}
// 如果 leafType 为 null,使用 expression.Type
var type = leafType ?? expression.Type;
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
return new FieldInfo(
path,
underlyingType,
leafName ?? string.Empty,
finalMapping?.IsKeyword ?? false,
finalMapping?.IsText ?? false,
finalMapping?.IsWildcard ?? false,
finalMapping?.IsNested ?? false || (mappingInfo?.IsNested(path) ?? false),
finalMapping?.IsDate ?? false,
finalMapping?.IsNumeric ?? false,
finalMapping?.IsBoolean ?? false,
finalMapping?.IsRange ?? false,
finalMapping?.Format,
finalMapping?.HasMultiFields ?? false
);
}
/// <summary>
/// 获取成员的实际类型
/// </summary>
private static Type GetMemberType(MemberInfo member)
{
return member switch
{
System.Reflection.FieldInfo field => field.FieldType,
PropertyInfo property => property.PropertyType,
MethodInfo method => method.ReturnType,
_ => typeof(object)
};
}
/// <summary>
/// 解析字段名称
/// </summary>
private static string ResolveFieldName(MemberInfo member)
{
// 检查 JsonPropertyName 属性
if (member is PropertyInfo property)
{
var jsonName = property.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonName != null && !string.IsNullOrWhiteSpace(jsonName.Name))
{
return jsonName.Name;
}
}
return member.Name;
}
#endregion
}

2
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IExpressionQueryService.cs

@ -32,6 +32,7 @@ public interface IExpressionQueryService
/// <param name="skipCount">跳过数据大小</param>
/// <param name="sourceExcludes">包含字段</param>
/// <param name="sourceIncludes">忽略字段</param>
/// <param name="beginMarker">排序起始字段</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<List<TDocument>> GetListAsync<TDocument>(
@ -42,5 +43,6 @@ public interface IExpressionQueryService
int skipCount = 0,
Fields? sourceExcludes = null,
Fields? sourceIncludes = null,
object[]? beginMarker = null,
CancellationToken cancellationToken = default) where TDocument : class;
}

8
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/README.md

@ -6,7 +6,6 @@ Abp Elasticsearch集成,提供全局唯一IElasticClient访问接口
## 模块引用
```csharp
[DependsOn(typeof(AbpElasticsearchModule))]
public class YouProjectModule : AbpModule
@ -15,6 +14,13 @@ public class YouProjectModule : AbpModule
}
```
## 接口定义
* [IIndexMappingProvider](./LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs) 获取索引映射属性
* [IExpressionQueryTranslator](./LINGYUN/Abp/Elasticsearch/IExpressionQueryTranslator.cs) 表达式树翻译为ES查询类
* [IExpressionQueryService](./LINGYUN/Abp/Elasticsearch/IExpressionQueryService.cs) 表达式树数据查询
* [IElasticsearchClientFactory](./LINGYUN/Abp/Elasticsearch/IElasticsearchClientFactory.cs) ES客户端管理
## 配置项
* AbpElasticsearchOptions.FieldCamelCase 字段是否采用 camelCase 格式, 默认false

122
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestModule.cs

@ -1,14 +1,7 @@
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;
using Elastic.Transport.Diagnostics.Auditing;
using LINGYUN.Abp.Tests;
using LINGYUN.Abp.Tests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
namespace LINGYUN.Abp.Elasticsearch;
@ -17,7 +10,6 @@ namespace LINGYUN.Abp.Elasticsearch;
typeof(AbpElasticsearchModule))]
public class AbpElasticsearchTestModule : AbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private const string UserSecretsId = "D4327320-718E-4A7F-A987-85838EDD8675";
public override void PreConfigureServices(ServiceConfigurationContext context)
@ -27,116 +19,4 @@ public class AbpElasticsearchTestModule : AbpModule
builder.AddUserSecrets(UserSecretsId);
}));
}
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
{
AsyncHelper.RunSync(async () => await OnPostApplicationInitializationAsync(context));
}
public async override Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
{
var clientFactory = context.ServiceProvider.GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexPatterns = new[] { TestDocumentIndexNames.Index + "*" };
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var dateTimeFormat = "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
var putTemplateResponse = await client.Indices.PutIndexTemplateAsync(indexTemplateName, setup =>
{
setup.IndexPatterns(indexPatterns);
setup.Priority(100);
setup.Version(1);
setup.Template(template =>
{
template.Settings(new IndexSettings()
{
NumberOfReplicas = 1,
NumberOfShards = 3,
Mapping = new MappingLimitSettings
{
TotalFields = new MappingLimitSettingsTotalFields
{
Limit = 1000,
},
NestedFields = new MappingLimitSettingsNestedFields
{
Limit = 50,
},
Depth = new MappingLimitSettingsDepth
{
Limit = 10,
},
}
});
template.Mappings(mp => mp
.Dynamic(DynamicMapping.False)
.Properties<TestDocument>(pd =>
{
pd.IntegerNumber(k => k.Id);
pd.Text(k => k.Name, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Text(t => t.Description);
pd.IntegerNumber(k => k.Age);
pd.DoubleNumber(k => k.Salary);
pd.Boolean(k => k.IsActive);
pd.Date(k => k.CreatedTime, d => d.Format(dateTimeFormat));
pd.Date(k => k.UpdatedTime, d => d.Format(dateTimeFormat));
pd.ByteNumber(k => k.Status);
pd.ByteNumber(k => k.NullableStatus);
pd.Text(k => k.StringValueStatus, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Keyword(k => k.Tags);
pd.Wildcard(k => k.Exceptions);
pd.Nested(n => n.Items, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.IntegerNumber(nameof(SubDocument.Id));
npd.Text(nameof(SubDocument.Name), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
npd.DoubleNumber(nameof(SubDocument.Price));
});
});
pd.Nested(n => n.Address, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.Text(nameof(Address.City), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(255))));
npd.Text(nameof(Address.Street));
});
});
}));
});
}, _cancellationTokenSource.Token);
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
await client.IndexAsync(
new TestDocument(),
dsl => dsl.Index(TestDocumentIndexNames.Index),
_cancellationTokenSource.Token);
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
AsyncHelper.RunSync(async () => await OnApplicationShutdownAsync(context));
}
public async override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
{
var clientFactory = context.ServiceProvider.GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
_cancellationTokenSource.Cancel();
}
}

5
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/DefaultExpressionQueryService_Tests.cs

@ -0,0 +1,5 @@
namespace LINGYUN.Abp.Elasticsearch;
public class DefaultExpressionQueryService_Tests : ExpressionQueryService_Tests<AbpElasticsearchTestModule>
{
}

169
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryService_Tests.cs

@ -0,0 +1,169 @@
using Elastic.Clients.Elasticsearch;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Modularity;
using Volo.Abp.Testing;
using Volo.Abp.Threading;
using Xunit;
namespace LINGYUN.Abp.Elasticsearch;
public abstract class ExpressionQueryService_Tests<TStartupModule> : AbpIntegratedTest<TStartupModule>
where TStartupModule : IAbpModule
{
private List<TestDocument> _documents = new List<TestDocument>();
protected IExpressionQueryService ExpressionQueryService { get; }
protected ExpressionQueryService_Tests()
{
ExpressionQueryService = GetRequiredService<IExpressionQueryService>();
}
#region 索引初始化
protected override void AfterInitialize()
{
AsyncHelper.RunSync(async () => await ApplicationInitializationAsync());
}
public override void Dispose()
{
AsyncHelper.RunSync(async () => await ApplicationShutdownAsync());
}
protected async virtual Task ApplicationInitializationAsync()
{
var clientFactory = GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
_documents.AddRange(new[]
{
new TestDocument
{
Name = "Name1",
Age = 20,
Address = new Address
{
City = "HANGZHOU",
},
CreatedTime = new DateTime(2026, 8, 1, 0, 0, 0),
Status = TestEnum.Active,
StringValueStatus = TestEnum.Pending,
IsActive = true,
Id = 1,
Salary = 3m,
},
new TestDocument
{
Name = "Name2",
Age = 10,
Address = new Address
{
City = "GUANGZHOU",
},
CreatedTime = new DateTime(2026, 5, 1, 0, 0, 0),
Status = TestEnum.Inactive,
StringValueStatus = TestEnum.Active,
IsActive = false,
Id = 2,
Salary = 7m,
Tags = new List<string>{ "B" },
},
new TestDocument
{
Name = "Test1",
Age = 30,
Address = new Address
{
City = "BEIJING",
},
CreatedTime = new DateTime(2026, 3, 1, 0, 0, 0),
Status = TestEnum.Pending,
StringValueStatus = TestEnum.Inactive,
IsActive = true,
Id = 3,
Salary = 10m,
Tags = new List<string> { "B", "C" },
},
});
await client.BulkAsync(b =>
b.Index(TestDocumentIndexNames.Index)
.Refresh(Refresh.WaitFor)
.IndexMany(_documents));
}
protected async virtual Task ApplicationShutdownAsync()
{
var clientFactory = GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index);
}
#endregion
[Fact]
public async Task Should_Get_Count()
{
(await ExpressionQueryService.GetCountAsync<TestDocument>(
TestDocumentIndexNames.Index,
x => x.Name!.StartsWith("Name") && x.Salary <= 10m)).ShouldBe(2);
(await ExpressionQueryService.GetCountAsync<TestDocument>(
TestDocumentIndexNames.Index,
x => !string.IsNullOrWhiteSpace(x.Name) && x.Tags != null && x.Tags.Contains("B"))).ShouldBe(2);
(await ExpressionQueryService.GetCountAsync<TestDocument>(
TestDocumentIndexNames.Index,
x =>
x.Name != null && x.Name.Contains("1") &&
(x.Status == TestEnum.Active || x.Status == TestEnum.Pending))).ShouldBe(2);
}
[Fact]
public async Task Should_Get_List()
{
var list1 = await ExpressionQueryService.GetListAsync<TestDocument>(
TestDocumentIndexNames.Index,
x => x.Name!.StartsWith("Name") && x.Salary <= 10m);
list1.Count.ShouldBe(2);
list1[0].Name.ShouldBe("Name1");
list1[0].Age.ShouldBe(20);
list1[0].Status.ShouldBe(TestEnum.Active);
list1[0].StringValueStatus.ShouldBe(TestEnum.Pending);
list1[0].IsActive.ShouldBeTrue();
list1[0].Tags.ShouldBeNull();
list1[0].Address.ShouldNotBeNull();
list1[0].Address!.City.ShouldBe("HANGZHOU");
var list2 = await ExpressionQueryService.GetListAsync<TestDocument>(
TestDocumentIndexNames.Index,
x => !string.IsNullOrWhiteSpace(x.Name) && x.Tags != null && x.Tags.Contains("B"));
list2.Count.ShouldBe(2);
list2[0].Name.ShouldBe("Name2");
list2[0].Age.ShouldBe(10);
list2[0].Status.ShouldBe(TestEnum.Inactive);
list2[0].StringValueStatus.ShouldBe(TestEnum.Active);
list2[0].IsActive.ShouldBeFalse();
list2[0].Tags.ShouldNotBeEmpty();
list2[0].Tags!.ShouldContain("B");
list2[0].Address!.City.ShouldBe("GUANGZHOU");
var list3 = await ExpressionQueryService.GetListAsync<TestDocument>(
TestDocumentIndexNames.Index,
x =>
x.Name != null && x.Name.Contains("1") &&
(x.Status == TestEnum.Active || x.Status == TestEnum.Pending));
list3.Count.ShouldBe(2);
list3[1].Name.ShouldBe("Test1");
list3[1].Age.ShouldBe(30);
list3[1].Status.ShouldBe(TestEnum.Pending);
list3[1].StringValueStatus.ShouldBe(TestEnum.Inactive);
list3[1].IsActive.ShouldBeTrue();
list3[1].Tags.ShouldNotBeEmpty();
list3[1].Tags!.ShouldContain("C");
list3[1].Address!.City.ShouldBe("BEIJING");
}
}

103
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs

@ -1,11 +1,15 @@
using Elastic.Clients.Elasticsearch.QueryDsl;
using Elastic.Clients.Elasticsearch.Mapping;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Modularity;
using Volo.Abp.Testing;
using Volo.Abp.Threading;
using Xunit;
namespace LINGYUN.Abp.Elasticsearch.Tests;
@ -13,6 +17,7 @@ namespace LINGYUN.Abp.Elasticsearch.Tests;
public abstract class ExpressionQueryTranslatorTests<TStartupModule> : AbpIntegratedTest<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly IExpressionQueryTranslator _expressionQueryTranslator;
public ExpressionQueryTranslatorTests()
@ -20,6 +25,102 @@ public abstract class ExpressionQueryTranslatorTests<TStartupModule> : AbpIntegr
_expressionQueryTranslator = GetRequiredService<IExpressionQueryTranslator>();
}
#region 索引初始化
protected override void AfterInitialize()
{
AsyncHelper.RunSync(async () => await ApplicationInitializationAsync());
}
public override void Dispose()
{
AsyncHelper.RunSync(async () => await ApplicationShutdownAsync());
}
protected async virtual Task ApplicationInitializationAsync()
{
var clientFactory = GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexPatterns = new[] { TestDocumentIndexNames.Index + "*" };
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var dateTimeFormat = "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
var putTemplateResponse = await client.Indices.PutIndexTemplateAsync(indexTemplateName, setup =>
{
setup.IndexPatterns(indexPatterns);
setup.Priority(100);
setup.Version(1);
setup.Template(template =>
{
template.Mappings(mp => mp
.Dynamic(DynamicMapping.False)
.Properties<TestDocument>(pd =>
{
pd.IntegerNumber(k => k.Id);
pd.Text(k => k.Name, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Text(t => t.Description);
pd.IntegerNumber(k => k.Age);
pd.DoubleNumber(k => k.Salary);
pd.Boolean(k => k.IsActive);
pd.Date(k => k.CreatedTime, d => d.Format(dateTimeFormat));
pd.Date(k => k.UpdatedTime, d => d.Format(dateTimeFormat));
pd.ByteNumber(k => k.Status);
pd.ByteNumber(k => k.NullableStatus);
pd.Text(k => k.StringValueStatus, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Keyword(k => k.Tags);
pd.Wildcard(k => k.Exceptions);
pd.Nested(n => n.Items, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.IntegerNumber(nameof(SubDocument.Id));
npd.Text(nameof(SubDocument.Name), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
npd.DoubleNumber(nameof(SubDocument.Price));
});
});
pd.Nested(n => n.Address, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.Text(nameof(Address.City), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(255))));
npd.Text(nameof(Address.Street));
});
});
}));
});
}, _cancellationTokenSource.Token);
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
await client.IndexAsync(
new TestDocument(),
dsl => dsl.Index(TestDocumentIndexNames.Index),
_cancellationTokenSource.Token);
}
protected async virtual Task ApplicationShutdownAsync()
{
var clientFactory = GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
_cancellationTokenSource.Cancel();
}
#endregion
#region 基础查询测试
[Fact]

Loading…
Cancel
Save