diff --git a/aspnet-core/LINGYUN.MicroService.All.slnx b/aspnet-core/LINGYUN.MicroService.All.slnx index 150884403..19b0fa78a 100644 --- a/aspnet-core/LINGYUN.MicroService.All.slnx +++ b/aspnet-core/LINGYUN.MicroService.All.slnx @@ -524,6 +524,7 @@ + diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs new file mode 100644 index 000000000..2a5847043 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogExpressionQueryTranslator.cs @@ -0,0 +1,374 @@ +using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.QueryDsl; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Volo.Abp; + +namespace LINGYUN.Abp.AuditLogging.Elasticsearch; + +/// +/// ES DSL表达式翻译器:把 翻译为 +/// Elastic.Clients.Elasticsearch 的 (QueryDsl)。 +/// +/// 支持的算子(超出即抛 ,fail loud): +/// +/// 逻辑:&&、||、!(映射为 bool filter / should+minimum_should_match / must_not) +/// 比较:==、!=、>、>=、<、<=(数值与日期映射为 term / range) +/// null 判断:字段 == null / != null(映射为 must_not exists / exists) +/// 字符串:Contains / StartsWith / EndsWith(映射为 wildcard)、Equals(映射为 term) +/// 集合:x.Actions.Any(predicate)(映射为 nested 查询或扁平字段展开) +/// 常量:true / false(映射为 match_all / match_none) +/// +/// +/// +internal class AuditLogExpressionQueryTranslator +{ + private bool _actionsIsNested; + private bool _caseInsensitiveWildcard; + private bool _appendKeywordForStringEquality; + + public AuditLogExpressionQueryTranslator( + bool actionsIsNested = false, + bool caseInsensitiveWildcard = true, + bool appendKeywordForStringEquality = true) + { + _actionsIsNested = actionsIsNested; + _caseInsensitiveWildcard = caseInsensitiveWildcard; + _appendKeywordForStringEquality = appendKeywordForStringEquality; + } + + public Query Translate(Expression> expression) + { + Check.NotNull(expression, nameof(expression)); + + return TranslateNode(expression.Body, prefix: null); + } + + private Query TranslateNode(Expression node, string? prefix) + { + return node switch + { + ConstantExpression { Value: bool value } => + value ? new MatchAllQuery() : new MatchNoneQuery(), + UnaryExpression { NodeType: ExpressionType.Not } unary => + (!TranslateNode(unary.Operand, prefix))!, + UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary => + TranslateNode(unary.Operand, prefix), + BinaryExpression binary => TranslateBinary(binary, prefix), + MethodCallExpression method => TranslateMethodCall(method, prefix), + MemberExpression member when member.Type == typeof(bool) => + new TermQuery { Field = ResolveField(member, prefix).Path, Value = true }, + _ => throw new NotSupportedException($"Unsupported expression node {node.NodeType}: {node}"), + }; + } + + private Query TranslateBinary(BinaryExpression node, string? prefix) + { + return node.NodeType switch + { + ExpressionType.AndAlso or ExpressionType.And => (Query)new BoolQuery + { + Filter = new Query[] { TranslateNode(node.Left, prefix), TranslateNode(node.Right, prefix) }, + }!, + ExpressionType.OrElse or ExpressionType.Or => (Query)new BoolQuery + { + Should = new Query[] { TranslateNode(node.Left, prefix), TranslateNode(node.Right, prefix) }, + MinimumShouldMatch = 1, + }!, + ExpressionType.Equal => TranslateComparison(node, prefix), + ExpressionType.NotEqual => (!TranslateComparison(node, prefix))!, + ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or ExpressionType.LessThan or ExpressionType.LessThanOrEqual => TranslateComparison(node, prefix), + _ => throw new NotSupportedException($"Unsupported binary operator {node.NodeType}: {node}"), + }; + } + + private Query TranslateComparison(BinaryExpression node, string? prefix) + { + var (fieldExpression, valueExpression) = ResolveOperands(node); + var field = ResolveField(fieldExpression, prefix); + + if (IsNullConstant(valueExpression)) + { + return new BoolQuery + { + MustNot = new Query[] { new ExistsQuery { Field = field.Path } }, + }; + } + + var value = Evaluate(valueExpression); + + return value == null + ? throw new NotSupportedException("The null value is only supported for the == null / != null comparison.") + : node.NodeType switch + { + ExpressionType.Equal => BuildEquality(field, value), + ExpressionType.GreaterThan => BuildRange(field, greaterThan: value), + ExpressionType.GreaterThanOrEqual => BuildRange(field, greaterThanOrEqualTo: value), + ExpressionType.LessThan => BuildRange(field, lessThan: value), + ExpressionType.LessThanOrEqual => BuildRange(field, lessThanOrEqualTo: value), + _ => throw new NotSupportedException($"Unsupported comparison operator {node.NodeType}"), + }; + } + + private static (Expression Field, Expression Value) ResolveOperands(BinaryExpression node) + { + var leftIsField = IsFieldLike(node.Left); + var rightIsField = IsFieldLike(node.Right); + if (leftIsField == rightIsField) + { + throw new NotSupportedException($"The comparison expression must have one side as a field and the other side as a value: {node}"); + } + return leftIsField ? (node.Left, node.Right) : (node.Right, node.Left); + } + + private static bool IsFieldLike(Expression expression) + { + var current = expression; + while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) + { + current = unary.Operand; + } + + while (current is MemberExpression member) + { + current = member.Expression!; + } + + return current is ParameterExpression; + } + + private static bool IsNullConstant(Expression expression) + { + while (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) + { + expression = unary.Operand; + } + + return expression is ConstantExpression { Value: null }; + } + + private Query BuildEquality(FieldRef field, object value) + { + if (field.Type == typeof(string)) + { + var fieldName = _appendKeywordForStringEquality + ? field.Path + ".keyword" + : field.Path; + return new TermQuery + { + Field = fieldName, + Value = (string)value, + CaseInsensitive = _caseInsensitiveWildcard, + }; + } + + if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) + { + var date = (DateTime)value; + return new DateRangeQuery + { + Field = field.Path, + Gte = date, + Lte = date, + }; + } + + return new TermQuery { Field = field.Path, Value = NormalizeValue(value) }; + } + + private static Query BuildRange( + FieldRef field, + object? greaterThan = null, + object? greaterThanOrEqualTo = null, + object? lessThan = null, + object? lessThanOrEqualTo = null) + { + if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) + { + var range = new DateRangeQuery { Field = field.Path }; + if (greaterThan != null) + { + range.Gt = (DateTime)greaterThan; + } + + if (greaterThanOrEqualTo != null) + { + range.Gte = (DateTime)greaterThanOrEqualTo; + } + + if (lessThan != null) + { + range.Lt = (DateTime)lessThan; + } + + if (lessThanOrEqualTo != null) + { + range.Lte = (DateTime)lessThanOrEqualTo; + } + + return range; + } + + var numberRange = new NumberRangeQuery { Field = field.Path }; + if (greaterThan != null) + { + numberRange.Gt = ToNumber(greaterThan); + } + + if (greaterThanOrEqualTo != null) + { + numberRange.Gte = ToNumber(greaterThanOrEqualTo); + } + + if (lessThan != null) + { + numberRange.Lt = ToNumber(lessThan); + } + + if (lessThanOrEqualTo != null) + { + numberRange.Lte = ToNumber(lessThanOrEqualTo); + } + + return numberRange; + } + + private Query BuildWildcard(string fieldName, string pattern) + { + return new WildcardQuery + { + Field = fieldName, + Value = pattern, + CaseInsensitive = _caseInsensitiveWildcard, + }; + } + + private Query TranslateMethodCall(MethodCallExpression node, string? prefix) + { + if (node.Method.DeclaringType == typeof(Enumerable) && node.Method.Name == nameof(Enumerable.Any)) + { + var collectionField = ResolveField(node.Arguments[0], prefix); + + Query inner; + if (node.Arguments.Count == 1) + { + inner = new ExistsQuery { Field = collectionField.Path }; + } + else + { + var predicate = UnwrapLambda(node.Arguments[1]); + inner = TranslateNode(predicate.Body, prefix: collectionField.Path); + } + + return _actionsIsNested + ? new NestedQuery(collectionField.Path, inner) + : inner; + } + + if (node.Method.DeclaringType == typeof(string) && node.Object != null) + { + var field = ResolveField(node.Object, prefix); + var value = (string)Evaluate(node.Arguments[0])!; + return node.Method.Name switch + { + nameof(string.Contains) => BuildWildcard(field.Path, "*" + EscapeWildcard(value) + "*"), + nameof(string.StartsWith) => BuildWildcard(field.Path, EscapeWildcard(value) + "*"), + nameof(string.EndsWith) => BuildWildcard(field.Path, "*" + EscapeWildcard(value)), + _ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"), + }; + } + + if (node.Method.Name == nameof(string.Equals)) + { + var fieldExpression = node.Object ?? node.Arguments[0]; + var valueExpression = node.Object != null ? node.Arguments[0] : node.Arguments[1]; + var field = ResolveField(fieldExpression, prefix); + return BuildEquality(field, (string)Evaluate(valueExpression)!); + } + + throw new NotSupportedException( + $"Unsupported method invocation {node.Method.DeclaringType?.Name}.{node.Method.Name}"); + } + + private static LambdaExpression UnwrapLambda(Expression expression) + { + while (expression is UnaryExpression { NodeType: ExpressionType.Quote } unary) + { + expression = unary.Operand; + } + return (LambdaExpression)expression; + } + + private static object? Evaluate(Expression expression) + { + return expression is ConstantExpression constant + ? constant.Value + : Expression.Lambda(expression).Compile().DynamicInvoke(); + } + + private static string EscapeWildcard(string value) + { + return value.Replace("\\", "\\\\").Replace("*", "\\*").Replace("?", "\\?"); + } + + private static FieldValue NormalizeValue(object value) + { + return value switch + { + string s => s, + bool b => b, + int i => i, + long l => l, + double d => d, + Guid g => g.ToString(), + Enum e => Convert.ToInt64(e), + _ => Convert.ToDouble(value), + }; + } + + private static Number? ToNumber(object? value) + { + if (value == null) + { + return null; + } + return value is double d ? d : Convert.ToInt64(value); + } + + private readonly record struct FieldRef(string Path, Type Type); + + private static FieldRef ResolveField(Expression expression, string? prefix) + { + var current = expression; + while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) + { + current = unary.Operand; + } + + var names = new Stack(); + while (current is MemberExpression member) + { + names.Push(member.Member.Name); + current = member.Expression!; + } + + if (current is not ParameterExpression) + { + 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; + } + + return new FieldRef(path, expression.Type); + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs index da5aa1dd0..d3195188e 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs @@ -1,4 +1,5 @@ using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Mapping; using Elastic.Clients.Elasticsearch.QueryDsl; using LINGYUN.Abp.Elasticsearch; using Microsoft.Extensions.Logging; @@ -11,6 +12,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.Specifications; using Volo.Abp.Timing; namespace LINGYUN.Abp.AuditLogging.Elasticsearch; @@ -39,6 +41,46 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen Logger = NullLogger.Instance; } + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + var client = _clientFactory.Create(); + var actionsIsNested = await GetActionsIsNested(client, cancellationToken); + var translator = new AuditLogExpressionQueryTranslator(actionsIsNested); + var query = translator.Translate(specification.ToExpression()); + + var response = await client.CountAsync(dsl => + dsl.Indices(CreateIndex()).Query(query), + cancellationToken); + + return response.Count; + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var client = _clientFactory.Create(); + var actionsIsNested = await GetActionsIsNested(client, cancellationToken); + var translator = new AuditLogExpressionQueryTranslator(actionsIsNested); + var query = translator.Translate(specification.ToExpression()); + + var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) + ? SortOrder.Asc : SortOrder.Desc; + sorting = !sorting.IsNullOrWhiteSpace() + ? sorting.Split()[0] + : nameof(AuditLog.ExecutionTime); + // ES最大支持10000, 超出这个长度后升级为使用Search_After方案 + + return skipCount >= 10000 + ? await SearchAfterAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken) + : await SearchFromSizeAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken); + } public async virtual Task GetCountAsync( DateTime? startTime = null, @@ -131,32 +173,12 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen hasException, httpStatusCode); - var searchResponse = await client.SearchAsync(dsl => - { - dsl.Indices(CreateIndex()) - .Query(new BoolQuery - { - Must = querys - }) - .Sort(s => s.Field(new FieldSort(GetField(sorting)) - { - Order = sortOrder - })) - .From(skipCount) - .Size(maxResultCount); + var query = new BoolQuery { Must = querys }; - // 字段过滤 - if (!includeDetails) - { - dsl.SourceExcludes( - ex => ex.Actions, - ex => ex.Comments, - ex => ex.Exceptions, - ex => ex.EntityChanges); - } - }, cancellationToken); - - return searchResponse.Documents.ToList(); + // ES最大支持10000, 超出这个长度后升级为使用Search_After方案 + return skipCount >= 10000 + ? await SearchAfterAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken) + : await SearchFromSizeAuditLogs(client, query, sorting, sortOrder, maxResultCount, skipCount, includeDetails, cancellationToken); } public async virtual Task GetAsync( @@ -318,12 +340,223 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen return queries; } + private async Task GetActionsIsNested(ElasticsearchClient client, CancellationToken cancellationToken = default) + { + var actionsIsNested = false; + + var response = await client.Indices.GetMappingAsync( + d => d.Indices(CreateIndex()), + cancellationToken); + + foreach (var mapping in response.Mappings) + { + if (mapping.Value.Mappings?.Properties is IDictionary properties && + properties.TryGetValue("Actions", out var actionsProperty)) + { + actionsIsNested = actionsProperty is NestedProperty; + break; + } + } + + return actionsIsNested; + } + + private async Task> SearchFromSizeAuditLogs( + ElasticsearchClient client, + Query query, + string sorting, + SortOrder sortOrder, + int maxResultCount, + int skipCount, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var searchResponse = await client.SearchAsync(dsl => + { + dsl.Indices(CreateIndex()) + .Query(query) + .Sort(s => s.Field(new FieldSort(GetField(sorting)) + { + Order = sortOrder + })) + .From(skipCount) + .Size(maxResultCount); + + if (!includeDetails) + { + dsl.SourceExcludes( + ex => ex.Actions, + ex => ex.Comments, + ex => ex.Exceptions, + ex => ex.EntityChanges); + } + }, cancellationToken); + + if (!searchResponse.IsSuccess()) + { + return []; + } + + return searchResponse.Documents.ToList(); + } + + private async Task> SearchAfterAuditLogs( + ElasticsearchClient client, + Query query, + string sorting, + SortOrder sortOrder, + int maxResultCount, + int skipCount, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var searchAfter = await GetSearchAfterValue( + client, + query, + sorting, + sortOrder, + skipCount, + cancellationToken); + + if (searchAfter == null || !searchAfter.Any()) + { + return []; + } + + var searchResponse = await client.SearchAsync(dsl => + { + dsl.Indices(CreateIndex()) + .Query(query) + .Sort(s => s.Field(new FieldSort(GetField(sorting)) + { + Order = sortOrder + })) + .Size(maxResultCount) + .SearchAfter(searchAfter); + + if (!includeDetails) + { + dsl.SourceExcludes( + ex => ex.Actions, + ex => ex.Comments, + ex => ex.Exceptions, + ex => ex.EntityChanges); + } + }, cancellationToken); + + if (!searchResponse.IsSuccess()) + { + return []; + } + + return searchResponse.Documents.ToList(); + } + + private async Task?> GetSearchAfterValue( + ElasticsearchClient client, + Query query, + string sorting, + SortOrder sortOrder, + int skipCount, + CancellationToken cancellationToken = default) + { + // 10000以内直接取最后一条数据 + if (skipCount < 10000) + { + var response = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(s => s.Field(new FieldSort(GetField(sorting)) + { + Order = sortOrder + })) + .SourceIncludes(x => x.Id) + .From(skipCount) + .Size(1), + cancellationToken); + + if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any()) + { + return null; + } + + var hit = response.Hits.FirstOrDefault(); + return hit?.Sort?.ToList(); + } + + // 获取第9999条数据Hits作为searchAfter + var firstResponse = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(s => s.Field(new FieldSort(GetField(sorting)) + { + Order = sortOrder + })) + .SourceIncludes(x => x.Id) + .From(9999) + .Size(1), + cancellationToken); + + if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any()) + { + return null; + } + + var firstHit = firstResponse.Hits.FirstOrDefault(); + if (firstHit?.Sort == null || !firstHit.Sort.Any()) + { + return null; + } + + var remaining = skipCount - 10000; + // 获取skipCount最近一条数据作为searchAfter + var secondResponse = await client.SearchAsync( + dsl => dsl.Indices(CreateIndex()) + .Query(query) + .Sort(s => s.Field(new FieldSort(GetField(sorting)) + { + Order = sortOrder + })) + .SourceIncludes(x => x.Id) + .SearchAfter(firstHit.Sort.ToList()) + .Size(1), + cancellationToken); + + if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any()) + { + return null; + } + + if (secondResponse.Hits.Count < remaining) + { + return null; + } + + var lastHit = secondResponse.Hits.LastOrDefault(); + if (lastHit?.Sort == null || !lastHit.Sort.Any()) + { + return null; + } + + return lastHit.Sort.ToList(); + } + protected virtual string CreateIndex() { return _indexNameNormalizer.NormalizeIndex("audit-log"); } - private readonly static IDictionary _fieldMaps = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + protected virtual string GetField(string field) + { + if (_auditLogFieldMaps.TryGetValue(field, out var mapField)) + { + return _elasticsearchOptions.FieldCamelCase ? mapField.ToCamelCase() : mapField.ToPascalCase(); + } + + return _elasticsearchOptions.FieldCamelCase ? field.ToCamelCase() : field.ToPascalCase(); + } + + private readonly static IDictionary _auditLogFieldMaps = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { { "Id", "Id.keyword" }, { "ApplicationName", "ApplicationName.keyword" }, @@ -344,13 +577,4 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen { "ExecutionTime", "ExecutionTime" }, { "HttpStatusCode", "HttpStatusCode" }, }; - protected virtual string GetField(string field) - { - if (_fieldMaps.TryGetValue(field, out var mapField)) - { - return _elasticsearchOptions.FieldCamelCase ? mapField.ToCamelCase() : mapField.ToPascalCase(); - } - - return _elasticsearchOptions.FieldCamelCase ? field.ToCamelCase() : field.ToPascalCase(); - } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs index 3e30cbf3a..7a86cb146 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogWriter.cs @@ -34,7 +34,7 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency _logger = logger; } - public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) { var client = _clientFactory.Create(); var auditLog = await _auditLogConverter.ConvertAsync(auditLogInfo); @@ -44,7 +44,7 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency .Id(auditLog.Id), cancellationToken); - if (!response.IsValidResponse) + if (!response.IsSuccess()) { _logger.LogWarning("Could not save the audit log object: " + Environment.NewLine + auditLog.ToString()); if (response.TryGetOriginalException(out var ex) && ex != null) @@ -55,7 +55,10 @@ public class ElasticsearchAuditLogWriter : IAuditLogWriter, ITransientDependency { _logger.LogWarning(response.ElasticsearchServerError.ToString()); } + return ""; } + + return auditLog.Id.ToString(); } public async virtual Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default) diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IsExternalInit.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IsExternalInit.cs new file mode 100644 index 000000000..ef904e6c2 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/IsExternalInit.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs index c5620565f..b39712998 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreModule.cs @@ -1,12 +1,16 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Mapperly; using Volo.Abp.Modularity; +using VoloAbpAuditLoggingEntityFrameworkCoreModule = Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule; +using VoloAbpIdentityEntityFrameworkCoreModule = Volo.Abp.Identity.EntityFrameworkCore.AbpIdentityEntityFrameworkCoreModule; +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; [DependsOn( - typeof(Volo.Abp.Identity.EntityFrameworkCore.AbpIdentityEntityFrameworkCoreModule), - typeof(Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule))] + typeof(VoloAbpIdentityEntityFrameworkCoreModule), + typeof(VoloAbpAuditLoggingEntityFrameworkCoreModule))] [DependsOn( typeof(AbpAuditLoggingModule), typeof(AbpMapperlyModule))] @@ -15,5 +19,10 @@ public class AbpAuditLoggingEntityFrameworkCoreModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddMapperlyObjectMapper(); + + context.Services.AddAbpDbContext(options => + { + options.AddRepository(); + }); } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs new file mode 100644 index 000000000..243ad5869 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogExpressionQueryConverter.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; + +namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; + +/// +/// 审计日志表达式树转换器 +/// +internal class AuditLogExpressionQueryConverter : ExpressionVisitor +{ + private readonly IReadOnlyDictionary _typeMap; + private readonly Dictionary _parameterMap = new(); + + public AuditLogExpressionQueryConverter() + : this(BuildDefaultTypeMap()) + { + } + + public AuditLogExpressionQueryConverter(IReadOnlyDictionary typeMap) + { + _typeMap = typeMap ?? throw new ArgumentNullException(nameof(typeMap)); + } + + public Expression> Convert(Expression> expression) + { + ArgumentNullException.ThrowIfNull(expression); + + _parameterMap.Clear(); + var rootParameter = Expression.Parameter(typeof(VoloAuditLog), expression.Parameters[0].Name); + _parameterMap[expression.Parameters[0]] = rootParameter; + + var body = Visit(expression.Body); + return Expression.Lambda>(body, rootParameter); + } + + protected override Expression VisitLambda(Expression node) + { + var parameters = node.Parameters.Select(p => + { + if (_parameterMap.TryGetValue(p, out var mapped)) + { + return mapped; + } + if (_typeMap.TryGetValue(p.Type, out var targetType)) + { + mapped = Expression.Parameter(targetType, p.Name); + _parameterMap[p] = mapped; + return mapped; + } + return p; + }).ToArray(); + + var body = Visit(node.Body); + return Expression.Lambda(body, parameters); + } + + protected override Expression VisitParameter(ParameterExpression node) + => _parameterMap.TryGetValue(node, out var mapped) ? mapped : node; + + protected override Expression VisitMember(MemberExpression node) + { + var expression = Visit(node.Expression); + + if (expression != null + && node.Member is PropertyInfo property + && _typeMap.ContainsKey(property.DeclaringType!)) + { + var targetProperty = expression.Type.GetProperty( + property.Name, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + + if (targetProperty == null) + { + throw new NotSupportedException( + $"The property {property.Name} could not be found on the target type {expression.Type.FullName} and thus the expression cannot be overridden."); + } + return Expression.MakeMemberAccess(expression, targetProperty); + } + + return node.Expression == expression + ? node + : Expression.MakeMemberAccess(expression, node.Member); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.IsGenericMethod) + { + var oldArguments = node.Method.GetGenericArguments(); + var newArguments = oldArguments + .Select(a => _typeMap.TryGetValue(a, out var mapped) ? mapped : a) + .ToArray(); + + if (!oldArguments.SequenceEqual(newArguments)) + { + var targetMethod = node.Method.GetGenericMethodDefinition().MakeGenericMethod(newArguments); + var instance = node.Object != null ? Visit(node.Object) : null; + var arguments = node.Arguments.Select(Visit).ToArray(); + return Expression.Call(instance, targetMethod, arguments!); + } + } + + return base.VisitMethodCall(node); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Quote) + { + var operand = Visit(node.Operand); + return Expression.Quote(operand); + } + return base.VisitUnary(node); + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (node.Value is Enum enumValue && _typeMap.TryGetValue(enumValue.GetType(), out var targetType)) + { + return Expression.Constant(Enum.ToObject(targetType, System.Convert.ToInt64(enumValue)), targetType); + } + return base.VisitConstant(node); + } + + private static Dictionary BuildDefaultTypeMap() + { + return new Dictionary + { + [typeof(AuditLog)] = typeof(VoloAuditLog), + [typeof(AuditLogAction)] = typeof(Volo.Abp.AuditLogging.AuditLogAction), + [typeof(EntityChange)] = typeof(Volo.Abp.AuditLogging.EntityChange), + [typeof(EntityPropertyChange)] = typeof(Volo.Abp.AuditLogging.EntityPropertyChange), + }; + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs similarity index 69% rename from aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs rename to aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs index 28cad7b10..e3b99b723 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/AuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogManager.cs @@ -3,23 +3,25 @@ using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; -using Volo.Abp.AuditLogging; using Volo.Abp.DependencyInjection; using Volo.Abp.ObjectMapping; +using Volo.Abp.Specifications; using Volo.Abp.Uow; +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; + namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; [Dependency(ReplaceServices = true)] -public class AuditLogManager : IAuditLogManager, ITransientDependency +public class EfCoreAuditLogManager : IAuditLogManager, ITransientDependency { protected IObjectMapper ObjectMapper { get; } - protected IAuditLogRepository AuditLogRepository { get; } + protected IEfCoreAuditLogRepository AuditLogRepository { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } - public AuditLogManager( - IAuditLogRepository auditLogRepository, + public EfCoreAuditLogManager( IUnitOfWorkManager unitOfWorkManager, + IEfCoreAuditLogRepository auditLogRepository, IObjectMapper objectMapper) { ObjectMapper = objectMapper; @@ -27,6 +29,39 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency UnitOfWorkManager = unitOfWorkManager; } + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + var converter = new AuditLogExpressionQueryConverter(); + var resetSpec = new ExpressionSpecification( + converter.Convert(specification.ToExpression())); + + return await AuditLogRepository.GetCountAsync(resetSpec, cancellationToken); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var converter = new AuditLogExpressionQueryConverter(); + var resetSpec = new ExpressionSpecification( + converter.Convert(specification.ToExpression())); + + var auditLogs = await AuditLogRepository.GetListAsync( + resetSpec, + sorting, + maxResultCount, + skipCount, + includeDetails, + cancellationToken); + + return ObjectMapper.Map, List>(auditLogs); + } public async virtual Task GetCountAsync( DateTime? startTime = null, @@ -105,7 +140,7 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency includeDetails, cancellationToken); - return ObjectMapper.Map, List>(auditLogs); + return ObjectMapper.Map, List>(auditLogs); } public async virtual Task GetAsync( @@ -115,7 +150,7 @@ public class AuditLogManager : IAuditLogManager, ITransientDependency { var auditLog = await AuditLogRepository.GetAsync(id, includeDetails, cancellationToken); - return ObjectMapper.Map(auditLog); + return ObjectMapper.Map(auditLog); } public async virtual Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs new file mode 100644 index 000000000..1c6f42879 --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.AuditLogging; +using Volo.Abp.AuditLogging.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Specifications; + +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; + +namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; + +public class EfCoreAuditLogRepository : Volo.Abp.AuditLogging.EntityFrameworkCore.EfCoreAuditLogRepository, IEfCoreAuditLogRepository +{ + public EfCoreAuditLogRepository( + IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(specification.ToExpression()) + .LongCountAsync(GetCancellationToken(cancellationToken)); + } + + public async virtual Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .IncludeDetails(includeDetails) + .Where(specification.ToExpression()) + .OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(VoloAuditLog.ExecutionTime)} DESC" : sorting) + .PageBy(skipCount, maxResultCount) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs index df0181d5b..9a058433d 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogWriter.cs @@ -31,7 +31,7 @@ public class EfCoreAuditLogWriter : IAuditLogWriter, ITransientDependency GuidGenerator = guidGenerator; } - public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + public async virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) { using (var uow = UnitOfWorkManager.Begin(true)) { @@ -40,6 +40,8 @@ public class EfCoreAuditLogWriter : IAuditLogWriter, ITransientDependency await AuditLogRepository.InsertAsync(auditLog); await uow.CompleteAsync(); + + return auditLog.Id.ToString(); } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/IEfCoreAuditLogRepository.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/IEfCoreAuditLogRepository.cs new file mode 100644 index 000000000..e9862a2fa --- /dev/null +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.EntityFrameworkCore/LINGYUN/Abp/AuditLogging/EntityFrameworkCore/IEfCoreAuditLogRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.AuditLogging; +using Volo.Abp.Specifications; + +using VoloAuditLog = Volo.Abp.AuditLogging.AuditLog; + +namespace LINGYUN.Abp.AuditLogging.EntityFrameworkCore; + +public interface IEfCoreAuditLogRepository : IAuditLogRepository +{ + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj index e99d9c8d5..ac0c392c1 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN.Abp.AuditLogging.csproj @@ -19,6 +19,7 @@ + diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs index e2efa0d7d..ed7a3fbaa 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/DefaultAuditLogManager.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.Auditing; using Volo.Abp.DependencyInjection; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.AuditLogging; @@ -98,4 +99,24 @@ public class DefaultAuditLogManager : IAuditLogManager, ISingletonDependency Logger.LogDebug("No audit log manager is available!"); return Task.CompletedTask; } + + public virtual Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + Logger.LogDebug("No audit log manager is available!"); + return Task.FromResult(0L); + } + + public Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + Logger.LogDebug("No audit log manager is available!"); + return Task.FromResult(new List()); + } } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs index d0838ad6b..bf92fae63 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Specifications; namespace LINGYUN.Abp.AuditLogging; @@ -55,8 +56,19 @@ public interface IAuditLogManager int? maxExecutionDuration = null, int? minExecutionDuration = null, bool? hasException = null, - HttpStatusCode? httpStatusCode = null, + HttpStatusCode? httpStatusCode = null, bool includeDetails = false, CancellationToken cancellationToken = default); + Task GetCountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + Task> GetListAsync( + ISpecification specification, + string? sorting = null, + int maxResultCount = 50, + int skipCount = 0, + bool includeDetails = false, + CancellationToken cancellationToken = default); } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs index 0fc2fb0b0..bdcf3c0f9 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/IAuditLogWriter.cs @@ -6,7 +6,7 @@ using Volo.Abp.Auditing; namespace LINGYUN.Abp.AuditLogging; public interface IAuditLogWriter { - Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default); + Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default); Task BulkWriteAsync(IEnumerable auditLogInfos, CancellationToken cancellationToken = default); } diff --git a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs index 873f28be2..dbe9e6b12 100644 --- a/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs +++ b/aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging/LINGYUN/Abp/AuditLogging/LoggerAuditLogWriter.cs @@ -24,10 +24,10 @@ public class LoggerAuditLogWriter : IAuditLogWriter, ISingletonDependency } } - public virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) + public virtual Task WriteAsync(AuditLogInfo auditLogInfo, CancellationToken cancellationToken = default) { _logger.LogInformation(auditLogInfo.ToString()); - return Task.CompletedTask; + return Task.FromResult(""); } } diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj index bba911522..a2cd57d31 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests.csproj @@ -4,7 +4,6 @@ net10.0 false - Debug;Release;PostgreSQL AnyCPU diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs index 8fd3da9ff..3b42d7145 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AbpAuditLoggingElasticsearchTestModule.cs @@ -1,8 +1,10 @@ +using Elastic.Clients.Elasticsearch; using LINGYUN.Abp.Elasticsearch; using LINGYUN.Abp.Tests; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using System; using Volo.Abp; using Volo.Abp.Modularity; @@ -13,26 +15,38 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch typeof(AbpAuditLoggingElasticsearchModule))] public class AbpAuditLoggingElasticsearchTestModule : AbpModule { + private const string UserSecretsId = "1748BEB4-4C7E-46F2-AE59-23956096B8E3"; + public override void PreConfigureServices(ServiceConfigurationContext context) { - var configurationOptions = new AbpConfigurationBuilderOptions + context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder => { - BasePath = @"D:\Projects\Development\Abp\AuditLogging\Elasticsearch", - EnvironmentName = "Development" - }; + builder.AddUserSecrets(UserSecretsId); + })); + } - context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(configurationOptions)); + public override void OnPostApplicationInitialization(ApplicationInitializationContext context) + { + RemoveTestIndexs(context.ServiceProvider); } public override void OnApplicationShutdown(ApplicationShutdownContext context) { - var options = context.ServiceProvider.GetRequiredService>().Value; - var clientFactory = context.ServiceProvider.GetRequiredService(); + RemoveTestIndexs(context.ServiceProvider); + } + + private static void RemoveTestIndexs(IServiceProvider serviceProvider) + { + var options = serviceProvider.GetRequiredService>().Value; + var clientFactory = serviceProvider.GetRequiredService(); var client = clientFactory.Create(); - var indicesResponse = client.Indices.Get($"{options.IndexPrefix}-security-log"); - foreach (var index in indicesResponse.Indices) + var indicesResponse = client.Indices.Get($"{options.IndexPrefix}-audit-log"); + if (indicesResponse.IsSuccess()) { - client.Indices.Delete(index.Key); + foreach (var index in indicesResponse.Indices) + { + client.Indices.Delete(index.Key); + } } } } diff --git a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs index befb33bb1..6fdc26be4 100644 --- a/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs +++ b/aspnet-core/tests/LINGYUN.Abp.AuditLogging.Elasticsearch.Tests/LINGYUN/Abp/AuditLogging/Elasticsearch/AuditLogManagerTests.cs @@ -1,19 +1,24 @@ using Moq.AutoMock; +using Newtonsoft.Json; using Shouldly; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Volo.Abp.Auditing; +using Volo.Abp.Specifications; using Xunit; namespace LINGYUN.Abp.AuditLogging.Elasticsearch { public class AuditLogManagerTests : AbpAuditLoggingElasticsearchTestBase { + private readonly IAuditLogWriter _writer; private readonly IAuditLogManager _manager; public AuditLogManagerTests() { + _writer = GetRequiredService(); _manager = GetRequiredService(); } @@ -23,7 +28,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch var mock = new AutoMocker(); var auditLogInfo = mock.CreateInstance(); - var id = await _manager.SaveAsync(auditLogInfo); + var id = await _writer.WriteAsync(auditLogInfo); id.ShouldNotBeNullOrWhiteSpace(); var findId = Guid.Parse(id); @@ -38,7 +43,11 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch [Fact] public async Task Save_Audit_Log_Should_Get_List() { - //await MockcAsync(10); + var count = 10; + await MockcAsync(count); + + // 延迟等待ES索引完成 + await Task.Delay(5000); // 异常应该只有3个 (await _manager.GetCountAsync( @@ -69,12 +78,66 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch var logs = await _manager.GetListAsync( userName: "_user_5", - clientId: "_client_5"); + clientId: "_client_5", + maxResultCount: count); + + logs.Count.ShouldBe(1); + logs[0].Url.ShouldBe("_url_5"); + logs[0].BrowserInfo.ShouldBe("_browser_5"); + logs[0].ApplicationName.ShouldBe("_app_5"); + + await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); + } + + [Fact] + public async Task Save_Audit_Log_Should_Get_List_With_Specification() + { + var count = 10; + await MockcAsync(count); + + // 延迟等待ES索引完成 + await Task.Delay(5000); + + // 异常应该只有3个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Exceptions != null))).ShouldBe(3); + + // 请求参数中包含 AAAAA 应该只有3个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Actions.Any(a => a.Parameters.Contains("AAAAA"))))).ShouldBe(3); + + // 正常可以查询7个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.Exceptions == null))).ShouldBe(7); + + // POST方法能查到5个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.HttpMethod == "POST"))).ShouldBe(5); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-1).AddHours(5)))).ShouldBe(6); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime <= DateTime.Now.AddDays(-1)))).ShouldBe(4); + + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.ExecutionTime >= DateTime.Now.AddDays(-3).AddHours(1) && + x.ExecutionTime <= DateTime.Now))).ShouldBe(8); + + // 索引5只存在一个 + (await _manager.GetCountAsync( + new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"))).ShouldBe(1); + + var logs = await _manager.GetListAsync( + new ExpressionSpecification(x => x.UserName == "_user_5" && x.ClientId == "_client_5"), + maxResultCount: count); logs.Count.ShouldBe(1); logs[0].Url.ShouldBe("_url_5"); logs[0].BrowserInfo.ShouldBe("_browser_5"); logs[0].ApplicationName.ShouldBe("_app_5"); + + await _manager.DeleteManyAsync(logs.Select(x => x.Id).ToList()); } protected async virtual Task> MockcAsync(int count) @@ -83,7 +146,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch var auditLogIds = new List(); - for (int i = 1; i <= count; i++) + for (var i = 1; i <= count; i++) { var auditLogInfo = mock.CreateInstance(); auditLogInfo.ClientId = $"_client_{i}"; @@ -92,10 +155,21 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch auditLogInfo.ApplicationName = $"_app_{i}"; auditLogInfo.BrowserInfo = $"_browser_{i}"; auditLogInfo.ExecutionTime = DateTime.Now; - if (i % 3 == 0) { auditLogInfo.Exceptions.Add(new Exception($"_exception_{i}")); + auditLogInfo.Actions.Add( + new AuditLogActionInfo + { + ServiceName = $"_service_{i}", + MethodName = $"_method_{i}", + ExecutionTime = DateTime.Now, + ExecutionDuration = new Random().Next(1, 1000), + Parameters = JsonConvert.SerializeObject(new + { + Paramter = "AAAAA", + }), + }); } if (i % 2 == 0) @@ -113,7 +187,7 @@ namespace LINGYUN.Abp.AuditLogging.Elasticsearch auditLogInfo.ExecutionTime = DateTime.Now.AddDays(-2); } - auditLogIds.Add(await _manager.SaveAsync(auditLogInfo)); + auditLogIds.Add(await _writer.WriteAsync(auditLogInfo)); } return auditLogIds;