diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 00000000..ed46252b --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,40 @@ +name: Publish NuGet Package + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Get the version from git tags + id: get_version + run: | + TAG=$(git describe --tags --abbrev=0) + VERSION=${TAG#v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Build the project + run: dotnet build --configuration Release --no-restore + + - name: Pack the NuGet package + run: dotnet pack --configuration Release --no-build --output ./nupkg -p:PackageVersion=${{ env.VERSION }} + + - name: Publish the NuGet package + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push ./nupkg/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json diff --git a/.github/workflows/test-with-code-coverage.yml b/.github/workflows/test-with-code-coverage.yml new file mode 100644 index 00000000..d1d39f5a --- /dev/null +++ b/.github/workflows/test-with-code-coverage.yml @@ -0,0 +1,55 @@ +name: Test with code coverage + +on: + push: + branches: + - '**' + +env: + DOTNET_INSTALL_DIR: "./.dotnet" + +jobs: + test: + runs-on: ubuntu-20.04 + permissions: + pull-requests: write + contents: write + services: + elasticsearch: + image: elasticsearch:7.17.0 + ports: + - 9200:9200 + options: -e="discovery.type=single-node" -e="xpack.security.enabled=false" --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '7.0' + - name: Verify Elasticsearch connection + env: + ELASTIC_SEARCH_URL: http://127.0.0.1:${{ job.services.elasticsearch.ports[9200] }} + run: | + echo $ELASTIC_SEARCH_URL + curl -fsSL "$ELASTIC_SEARCH_URL/_cat/health?h=status" + - name: Install dependencies + run: dotnet restore --verbosity quiet + + - name: Build + run: dotnet build --no-restore /clp:ErrorsOnly /p:GeneratePackageOnBuild=false --verbosity quiet + + - name: Test + run: | + for name in `ls ./test/*.Tests/*.csproj | awk '{print $NF}'`; + do + dotnet test ${name} --no-restore --no-build --logger trx --settings CodeCoverage.runsettings --results-directory coverage --collect:"XPlat Code Coverage" + done + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + files: coverage/*/coverage.cobertura.xml + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/src/AElf.EntityMapping.Elasticsearch/ElasticsearchCollectionNameProvider.cs b/src/AElf.EntityMapping.Elasticsearch/ElasticsearchCollectionNameProvider.cs index e4a6dc42..2d45ff20 100644 --- a/src/AElf.EntityMapping.Elasticsearch/ElasticsearchCollectionNameProvider.cs +++ b/src/AElf.EntityMapping.Elasticsearch/ElasticsearchCollectionNameProvider.cs @@ -38,19 +38,17 @@ protected override async Task> GetCollectionNameAsync(List { GetDefaultCollectionName() }; - - var shardKeyCollectionNames = await _shardingKeyProvider.GetCollectionNameAsync(conditions); - var routeKeyCollectionNames = - await _collectionRouteKeyProvider.GetCollectionNameAsync(conditions); - if (shardKeyCollectionNames.Count > 0 && routeKeyCollectionNames.Count > 0) + var shardKeyCollectionNames = await _shardingKeyProvider.GetCollectionNameAsync(conditions); + if (shardKeyCollectionNames.IsNullOrEmpty()) { - return shardKeyCollectionNames.Intersect(routeKeyCollectionNames).ToList(); + return await _collectionRouteKeyProvider.GetCollectionNameAsync(conditions); } - return shardKeyCollectionNames.Concat(routeKeyCollectionNames).ToList(); + + return shardKeyCollectionNames; } - protected override async Task> GetCollectionNameByEntityAsync(TEntity entity) + protected override async Task> GetCollectionNameByEntityAsync(TEntity entity) { if (entity == null) return new List { GetDefaultCollectionName() }; @@ -65,7 +63,7 @@ protected override async Task> GetCollectionNameByEntityAsync(List< { if (entities == null || entities.Count == 0) return new List { GetDefaultCollectionName() }; - + return _shardingKeyProvider.IsShardingCollection() ? await _shardingKeyProvider.GetCollectionNameAsync(entities) : new List { GetDefaultCollectionName() }; @@ -73,7 +71,7 @@ protected override async Task> GetCollectionNameByEntityAsync(List< protected override async Task GetCollectionNameByIdAsync(TKey id) { - if (!_shardingKeyProvider.IsShardingCollection()) + if (!_shardingKeyProvider.IsShardingCollection()) return GetDefaultCollectionName(); return await _collectionRouteKeyProvider.GetCollectionNameAsync(id.ToString()); } diff --git a/src/AElf.EntityMapping.Elasticsearch/IElasticsearchClientProvider.cs b/src/AElf.EntityMapping.Elasticsearch/IElasticsearchClientProvider.cs index 988f51df..89b33684 100644 --- a/src/AElf.EntityMapping.Elasticsearch/IElasticsearchClientProvider.cs +++ b/src/AElf.EntityMapping.Elasticsearch/IElasticsearchClientProvider.cs @@ -1,3 +1,4 @@ +using System.Text; using AElf.EntityMapping.Elasticsearch.Options; using Elasticsearch.Net; using Microsoft.Extensions.Options; @@ -20,6 +21,20 @@ public ElasticsearchClientProvider(IOptions options) var uris = options.Value.Uris.ConvertAll(x => new Uri(x)); var connectionPool = new StaticConnectionPool(uris); var settings = new ConnectionSettings(connectionPool); + // .DisableDirectStreaming(); + // .OnRequestCompleted(callDetails => + // { + // // Print Request DSL + // if (callDetails.RequestBodyInBytes != null) + // { + // Console.WriteLine($"Request JSON: {Encoding.UTF8.GetString(callDetails.RequestBodyInBytes)}"); + // } + // // // Print Response Data + // // if (callDetails.ResponseBodyInBytes != null) + // // { + // // Console.WriteLine($"Response JSON: {Encoding.UTF8.GetString(callDetails.ResponseBodyInBytes)}"); + // // } + // }); _elasticClient = new ElasticClient(settings); } diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticGeneratorQueryModelVisitor.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticGeneratorQueryModelVisitor.cs index 7f8db7a5..b8562067 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticGeneratorQueryModelVisitor.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticGeneratorQueryModelVisitor.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Linq.Expressions; +using AElf.EntityMapping.Linq; using Nest; using Remotion.Linq; using Remotion.Linq.Clauses; @@ -25,8 +26,8 @@ public QueryAggregator GenerateElasticQuery(QueryModel queryModel) QueryAggregator = new QueryAggregator(); VisitQueryModel(queryModel); return QueryAggregator; - } - + } + public override void VisitQueryModel(QueryModel queryModel) { queryModel.SelectClause.Accept(this, queryModel); @@ -34,17 +35,17 @@ public override void VisitQueryModel(QueryModel queryModel) VisitBodyClauses(queryModel.BodyClauses, queryModel); VisitResultOperators(queryModel.ResultOperators, queryModel); } - + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) { if (fromClause.FromExpression is SubQueryExpression subQueryExpression) { VisitQueryModel(subQueryExpression.QueryModel); } - + base.VisitMainFromClause(fromClause, queryModel); } - + public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index) { var tree = new GeneratorExpressionTreeVisitor(_propertyNameInferrerParser); @@ -66,62 +67,110 @@ public override void VisitWhereClause(WhereClause whereClause, QueryModel queryM QueryAggregator.Query = query; } + base.VisitWhereClause(whereClause, queryModel, index); } - + protected override void VisitResultOperators(ObservableCollection resultOperators, QueryModel queryModel) { foreach (var resultOperator in resultOperators) { - if (resultOperator is SkipResultOperator skipResultOperator) + switch (resultOperator) { - QueryAggregator.Skip = skipResultOperator.GetConstantCount(); - } - - if (resultOperator is TakeResultOperator takeResultOperator) - { - QueryAggregator.Take = takeResultOperator.GetConstantCount(); - } - - if (resultOperator is GroupResultOperator groupResultOperator) - { - var members = new List>(); - - switch (groupResultOperator.KeySelector) + case SkipResultOperator skipResultOperator: + QueryAggregator.Skip = skipResultOperator.GetConstantCount(); + break; + case TakeResultOperator takeResultOperator: + QueryAggregator.Take = takeResultOperator.GetConstantCount(); + break; + case GroupResultOperator groupResultOperator: { - case MemberExpression memberExpression: - members.Add(new Tuple(memberExpression.Member.Name, memberExpression.Type)); - break; - case NewExpression newExpression: - members.AddRange(newExpression.Arguments - .Cast() - .Select(memberExpression => new Tuple(memberExpression.Member.Name, memberExpression.Type))); - break; + var members = new List>(); + + switch (groupResultOperator.KeySelector) + { + case MemberExpression memberExpression: + members.Add(new Tuple(GetFullNameKey(memberExpression), memberExpression.Type)); + break; + case NewExpression newExpression: + members.AddRange(newExpression.Arguments + .Cast() + .Select(memberExpression => new Tuple(GetFullNameKey(memberExpression), memberExpression.Type))); + break; + } + + members.ForEach(property => { QueryAggregator.GroupByExpressions.Add(new GroupByProperties(property.Item1, property.Item2)); }); + break; } - - members.ForEach(property => - { - QueryAggregator.GroupByExpressions.Add(new GroupByProperties(property.Item1, property.Item2)); - }); + case AfterResultOperator afterResultOperator: + QueryAggregator.After = afterResultOperator.GetConstantPosition(); + break; } } - + base.VisitResultOperators(resultOperators, queryModel); } - + + private string GetFullNameKey(MemberExpression memberExpression) + { + var key = _propertyNameInferrerParser.Parser(memberExpression.Member.Name); + while (memberExpression.Expression != null) + { + memberExpression = memberExpression.Expression as MemberExpression; + if (memberExpression == null) + { + break; + } + + key = _propertyNameInferrerParser.Parser(memberExpression.Member.Name) + "." + key; + return key; + } + + return key; + } + + public override void VisitOrderByClause(OrderByClause orderByClause, QueryModel queryModel, int index) { foreach (var ordering in orderByClause.Orderings) { - var memberExpression = (MemberExpression) ordering.Expression; + var memberExpression = (MemberExpression)ordering.Expression; var direction = orderByClause.Orderings[0].OrderingDirection; - var propertyName = memberExpression.Member.Name; - var type = memberExpression.Type; - QueryAggregator.OrderByExpressions.Add(new OrderProperties(propertyName, type, direction)); + //get full property path if there is sub object + string propertyName = GetFullPropertyPath(memberExpression); + + if (!string.IsNullOrEmpty(propertyName)) + { + var type = memberExpression.Type; + QueryAggregator.OrderByExpressions.Add(new OrderProperties(propertyName, type, direction)); + } } - + base.VisitOrderByClause(orderByClause, queryModel, index); } + + private string GetFullPropertyPath(Expression expression) + { + switch (expression) + { + case MemberExpression memberExpression: + var parentPath = GetFullPropertyPath(memberExpression.Expression); + var currentMemberName = _propertyNameInferrerParser.Parser(memberExpression.Member.Name); + return string.IsNullOrEmpty(parentPath) ? currentMemberName : $"{parentPath}.{currentMemberName}"; + + case MethodCallExpression methodCallExpression: + // Handles method calls like 'get_Item', which are usually associated with indexed access to collections + if (methodCallExpression.Method.Name.Equals("get_Item") && methodCallExpression.Object != null) + { + // Assuming this is an indexed access to an array or list, we will ignore the index and use only the name of the collection + var collectionPath = GetFullPropertyPath(methodCallExpression.Object); + return collectionPath; // Returns the path of the collection directly, without adding an index + } + break; + } + + return null; + } } } \ No newline at end of file diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryExecutor.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryExecutor.cs index 3a5ffb9c..e6cfe552 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryExecutor.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryExecutor.cs @@ -86,7 +86,12 @@ public IEnumerable ExecuteCollection(QueryModel queryModel) descriptor.Take(take); descriptor.Size(take); } - + + if (queryAggregator.After != null) + { + descriptor.SearchAfter(queryAggregator.After); + } + if (queryAggregator.Query != null) { descriptor.Query(q => queryAggregator.Query); @@ -95,7 +100,7 @@ public IEnumerable ExecuteCollection(QueryModel queryModel) { descriptor.MatchAll(); } - + if (queryAggregator.OrderByExpressions.Any()) { @@ -103,8 +108,7 @@ public IEnumerable ExecuteCollection(QueryModel queryModel) { foreach (var orderByExpression in queryAggregator.OrderByExpressions) { - var property = _propertyNameInferrerParser.Parser(orderByExpression.PropertyName) + - orderByExpression.GetKeywordIfNecessary(); + var property = _propertyNameInferrerParser.Parser(orderByExpression.PropertyName); d.Field(property, orderByExpression.OrderingDirection == OrderingDirection.Asc ? SortOrder.Ascending @@ -140,7 +144,7 @@ public IEnumerable ExecuteCollection(QueryModel queryModel) }); } - + // var dsl = _elasticClient.RequestResponseSerializer.SerializeToString(descriptor); return descriptor; }); @@ -238,7 +242,6 @@ public T ExecuteScalar(QueryModel queryModel) { descriptor.Query(q => queryAggregator.Query); } - // var dsl = _elasticClient.RequestResponseSerializer.SerializeToString(descriptor); return descriptor; }); if (!response.IsValid) @@ -312,6 +315,22 @@ private string GetIndexName(QueryModel queryModel) AsyncHelper.RunSync(async () => await _collectionNameProvider.GetFullCollectionNameAsync(conditions)); return IndexNameHelper.FormatIndexName(indexNames); } + + private static string GetCollectionNameKey(MemberExpression memberExpression) + { + string key = memberExpression.Member.Name; + while (memberExpression.Expression != null) + { + memberExpression = memberExpression.Expression as MemberExpression; + if (memberExpression == null) + { + break; + } + key = memberExpression.Member.Name +"."+ key; + return key; + } + return key; + } } public class Grouping : IGrouping @@ -336,4 +355,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryable.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryable.cs index 16072056..074c5bd8 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryable.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/ElasticsearchQueryable.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; +using AElf.EntityMapping.Linq; using Nest; using Remotion.Linq; -using Remotion.Linq.Parsing.Structure; using Volo.Abp.Domain.Entities; namespace AElf.EntityMapping.Elasticsearch.Linq @@ -11,7 +11,8 @@ public class ElasticsearchQueryable : QueryableBase, IElasticsearchQueryab { public ElasticsearchQueryable(IElasticClient elasticClient, ICollectionNameProvider collectionNameProvider, string index) - : base(new DefaultQueryProvider(typeof(ElasticsearchQueryable<>), QueryParser.CreateDefault(), + : base(new DefaultQueryProvider(typeof(ElasticsearchQueryable<>), + QueryParserFactory.Create(), new ElasticsearchQueryExecutor(elasticClient, collectionNameProvider, index))) { } diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/GeneratorExpressionTreeVisitor.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/GeneratorExpressionTreeVisitor.cs index 940712e2..7aa1750a 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/GeneratorExpressionTreeVisitor.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/GeneratorExpressionTreeVisitor.cs @@ -127,12 +127,53 @@ protected override Expression VisitMethodCall(MethodCallExpression expression) return expression; } + // private string GetFullNameKey(MemberExpression memberExpression) + // { + // var key = _propertyNameInferrerParser.Parser(memberExpression.Member.Name); + // while (memberExpression.Expression != null) + // { + // memberExpression = memberExpression.Expression as MemberExpression; + // if (memberExpression == null) + // { + // break; + // } + // + // key = _propertyNameInferrerParser.Parser(memberExpression.Member.Name + "." + key); + // return key; + // } + // + // return key; + // } + + private string GetFullPropertyPath(Expression expression) + { + switch (expression) + { + case MemberExpression memberExpression: + var parentPath = GetFullPropertyPath(memberExpression.Expression); + var currentMemberName = _propertyNameInferrerParser.Parser(memberExpression.Member.Name); + return string.IsNullOrEmpty(parentPath) ? currentMemberName : $"{parentPath}.{currentMemberName}"; + + case MethodCallExpression methodCallExpression: + // Handles method calls like 'get_Item', which are usually associated with indexed access to collections + if (methodCallExpression.Method.Name.Equals("get_Item") && methodCallExpression.Object != null) + { + // Assuming this is an indexed access to an array or list, we will ignore the index and use only the name of the collection + var collectionPath = GetFullPropertyPath(methodCallExpression.Object); + return collectionPath; // Returns the path of the collection directly, without adding an index + } + break; + } + + return null; + } + protected override Expression VisitMember(MemberExpression expression) { Visit(expression.Expression); PropertyType = Nullable.GetUnderlyingType(expression.Type) ?? expression.Type; - PropertyName = _propertyNameInferrerParser.Parser(expression.Member.Name); + PropertyName = _propertyNameInferrerParser.Parser(GetFullPropertyPath(expression)); // Implicit boolean is only a member visit if (expression.Type == typeof(bool)) @@ -188,9 +229,9 @@ protected override Expression VisitSubQuery(SubQueryExpression expression) { Visit(whereClause.Predicate); Node tmp = (Node)QueryMap[whereClause.Predicate].Clone(); - QueryMap[expression] = tmp ; + QueryMap[expression] = tmp; QueryMap[expression].IsSubQuery = true; - QueryMap[expression].SubQueryPath = from;//from.ToLower(); + QueryMap[expression].SubQueryPath = from; //from.ToLower(); QueryMap[expression].SubQueryFullPath = fullPath; // VisitBinarySetSubQuery((BinaryExpression)whereClause.Predicate, from, fullPath, true); BinaryExpression predicate = (BinaryExpression)whereClause.Predicate; @@ -198,6 +239,7 @@ protected override Expression VisitSubQuery(SubQueryExpression expression) { VisitBinarySetSubQuery((BinaryExpression)predicate.Left, from, fullPath, true); } + if (predicate.Right is BinaryExpression) { VisitBinarySetSubQuery((BinaryExpression)predicate.Right, from, fullPath, true); @@ -380,10 +422,8 @@ private Node HandleStringProperty(Expression expression) { case ExpressionType.Equal: return new TermNode(PropertyName, Value); - // return new MatchPhraseNode(PropertyName, Value); case ExpressionType.NotEqual: return new NotNode(new TermNode(PropertyName, Value)); - //return new NotNode(new MatchPhraseNode(PropertyName, Value)); default: return null; } @@ -556,9 +596,9 @@ private object ConvertEnumValue(Type entityType, string propertyName, object val return (int)enumValue; } + protected void VisitBinarySetSubQuery(BinaryExpression expression, string path, string fullPath, bool parentIsSubQuery) { - if (expression.Left is BinaryExpression && expression.Right is ConstantExpression) { return; diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/QueryAggregator.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/QueryAggregator.cs index 5a82752d..05524841 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/QueryAggregator.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/QueryAggregator.cs @@ -11,6 +11,7 @@ public class QueryAggregator public List PropertiesToSelect = new List(); public List OrderByExpressions = new List(); public List GroupByExpressions = new List(); + public object[] After { get; set; } } public class OrderProperties diff --git a/src/AElf.EntityMapping.Elasticsearch/Linq/QueryModelExtensions.cs b/src/AElf.EntityMapping.Elasticsearch/Linq/QueryModelExtensions.cs index 3ff88165..728fa583 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Linq/QueryModelExtensions.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Linq/QueryModelExtensions.cs @@ -17,36 +17,45 @@ public static List GetCollectionNameConditions(this Que private static void VisitQueryModel(List conditions, QueryModel queryModel) { var whereClauses = queryModel.BodyClauses.OfType().ToList(); - foreach (var predicate in whereClauses.Select(whereClause => (BinaryExpression)whereClause.Predicate)) + foreach (var predicate in whereClauses.Select(whereClause => whereClause.Predicate)) { - switch (predicate.Left) + switch (predicate) { - case BinaryExpression left: - VisitBinaryExpression(conditions, left); - break; - case SubQueryExpression leftSub: - VisitQueryModel(conditions, leftSub.QueryModel); - break; - } - - switch (predicate.Right) - { - case BinaryExpression right: - VisitBinaryExpression(conditions, right); + case BinaryExpression binaryExpression: + switch (binaryExpression.Left) + { + case BinaryExpression left: + VisitBinaryExpression(conditions, left); + break; + case SubQueryExpression leftSub: + VisitQueryModel(conditions, leftSub.QueryModel); + break; + } + + switch (binaryExpression.Right) + { + case BinaryExpression right: + VisitBinaryExpression(conditions, right); + break; + case SubQueryExpression rightSub: + VisitQueryModel(conditions, rightSub.QueryModel); + break; + } + + if (binaryExpression.Left is not BinaryExpression + && binaryExpression.Left is not SubQueryExpression + && binaryExpression.Right is not BinaryExpression + && binaryExpression.Right is not SubQueryExpression + && binaryExpression is BinaryExpression p) + { + VisitBinaryExpression(conditions, p); + } + break; - case SubQueryExpression rightSub: - VisitQueryModel(conditions, rightSub.QueryModel); + case SubQueryExpression subQueryExpression: + VisitQueryModel(conditions, subQueryExpression.QueryModel); break; } - - if (predicate.Left is not BinaryExpression - && predicate.Left is not SubQueryExpression - && predicate.Right is not BinaryExpression - && predicate.Right is not SubQueryExpression - && predicate is BinaryExpression p) - { - VisitBinaryExpression(conditions, p); - } } } @@ -56,16 +65,16 @@ private static void VisitBinaryExpression(List conditio { var memberExpression = expression.Left as MemberExpression; var constantExpression = expression.Right as ConstantExpression; - conditions.Add(new CollectionNameCondition { - Key = memberExpression.Member.Name, + Key = GetCollectionNameKey(memberExpression), Value = constantExpression.Value, Type = GetConditionType(expression.NodeType) }); + return; } - + switch (expression.Left) { case SubQueryExpression leftSub: @@ -105,4 +114,22 @@ private static ConditionType GetConditionType(ExpressionType expressionType) throw new ArgumentOutOfRangeException(nameof(expressionType), expressionType, null); } } + + private static string GetCollectionNameKey(MemberExpression memberExpression) + { + var key = memberExpression.Member.Name; + while (memberExpression.Expression != null) + { + memberExpression = memberExpression.Expression as MemberExpression; + if (memberExpression == null) + { + break; + } + + key = memberExpression.Member.Name + "." + key; + return key; + } + + return key; + } } \ No newline at end of file diff --git a/src/AElf.EntityMapping.Elasticsearch/Services/ElasticIndexService.cs b/src/AElf.EntityMapping.Elasticsearch/Services/ElasticIndexService.cs index 2aeb01d3..d20e916e 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Services/ElasticIndexService.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Services/ElasticIndexService.cs @@ -38,7 +38,7 @@ public Task GetElasticsearchClientAsync() return Task.FromResult(_elasticsearchClientProvider.GetClient()); } - public async Task CreateIndexAsync(string indexName, Type type, int shard = 1, int numberOfReplicas = 1) + public async Task CreateIndexAsync(string indexName, Type type, int shard = 1, int numberOfReplicas = 1, Dictionary indexSettings = null) { if (!type.IsClass || type.IsAbstract || !typeof(IEntityMappingEntity).IsAssignableFrom(type)) { @@ -60,8 +60,19 @@ public async Task CreateIndexAsync(string indexName, Type type, int shard = 1, i ss => ss.Index(indexName) .Settings( - o => o.NumberOfShards(shard).NumberOfReplicas(numberOfReplicas) - .Setting("max_result_window", _elasticsearchOptions.MaxResultWindow)) + o => + { + var setting = o.NumberOfShards(shard).NumberOfReplicas(numberOfReplicas) + .Setting("max_result_window", _elasticsearchOptions.MaxResultWindow); + if (indexSettings != null) + { + foreach (var indexSetting in indexSettings) + { + setting.Setting(indexSetting.Key, indexSetting.Value); + } + } + return setting; + }) .Map(m => m.AutoMap(type))); if (!result.Acknowledged) throw new ElasticsearchException($"Create Index {indexName} failed : " + @@ -135,4 +146,41 @@ public async Task CreateCollectionRouteKeyIndexAsync(Type type, int shard = 1, i } } } + + public async Task DeleteIndexAsync(string collectionName = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(collectionName)) + { + throw new ArgumentNullException(nameof(collectionName), "Collection name must be provided."); + } + + try + { + var elasticClient = await GetElasticsearchClientAsync(); + var response = await elasticClient.Indices.DeleteAsync(collectionName, ct: cancellationToken); + if (!response.IsValid) + { + if (response.ServerError == null) + { + return; + } + + if (response.ServerError?.Status == 404) + { + _logger.LogError("Failed to delete index {0} does not exist.", collectionName); + return; + } + + // Log the error or throw an exception based on the response + throw new ElasticsearchException($"Failed to delete index {collectionName}: {response.ServerError.Error.Reason}"); + } + + _logger.LogInformation("Index {0} deleted successfully.", collectionName); + } + catch (Exception ex) + { + // Handle exceptions from the client (network issues, etc.) + throw new ElasticsearchException($"An error occurred while delete index {collectionName}: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/src/AElf.EntityMapping.Elasticsearch/Services/EnsureIndexBuildService.cs b/src/AElf.EntityMapping.Elasticsearch/Services/EnsureIndexBuildService.cs index 3b4fe710..5bfdf065 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Services/EnsureIndexBuildService.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Services/EnsureIndexBuildService.cs @@ -10,14 +10,14 @@ namespace AElf.EntityMapping.Elasticsearch.Services; -public class EnsureIndexBuildService: IEnsureIndexBuildService, ITransientDependency +public class EnsureIndexBuildService : IEnsureIndexBuildService, ITransientDependency { private readonly IElasticIndexService _elasticIndexService; private readonly List _modules; private readonly ElasticsearchOptions _elasticsearchOptions; private readonly AElfEntityMappingOptions _entityMappingOptions; - - + + public EnsureIndexBuildService(IOptions moduleConfiguration, IElasticIndexService elasticIndexService, IOptions entityMappingOptions, @@ -28,7 +28,7 @@ public EnsureIndexBuildService(IOptions moduleConfigura _elasticsearchOptions = elasticsearchOptions.Value; _entityMappingOptions = entityMappingOptions.Value; } - + public void EnsureIndexesCreate() { AsyncHelper.RunSync(async () => @@ -39,19 +39,19 @@ public void EnsureIndexesCreate() } }); } - + private async Task HandleModuleAsync(Type moduleType) { var types = GetTypesAssignableFrom(moduleType.Assembly); foreach (var t in types) { - var indexName = IndexNameHelper.GetDefaultFullIndexName(t,_entityMappingOptions.CollectionPrefix); - + var indexName = IndexNameHelper.GetDefaultFullIndexName(t, _entityMappingOptions.CollectionPrefix); + if (IsShardingCollection(t)) { //if shard index, create index Template var indexTemplateName = indexName + "-template"; - await _elasticIndexService.CreateIndexTemplateAsync(indexTemplateName,indexName, t, + await _elasticIndexService.CreateIndexTemplateAsync(indexTemplateName, indexName, t, _elasticsearchOptions.NumberOfShards, _elasticsearchOptions.NumberOfReplicas); //create index marked field cache @@ -66,9 +66,7 @@ await _elasticIndexService.CreateCollectionRouteKeyIndexAsync(t, _elasticsearchO await _elasticIndexService.CreateIndexAsync(indexName, t, _elasticsearchOptions.NumberOfShards, _elasticsearchOptions.NumberOfReplicas); } - } - } private async Task CreateShardingCollectionTailIndexAsync() @@ -86,7 +84,7 @@ private List GetTypesAssignableFrom(Assembly assembly) !type.IsAbstract && type.IsClass && compareType != type) .Cast().ToList(); } - + private bool IsShardingCollection(Type type) { if (_entityMappingOptions == null || _entityMappingOptions.ShardInitSettings == null) @@ -94,6 +92,4 @@ private bool IsShardingCollection(Type type) var options = _entityMappingOptions.ShardInitSettings.Find(a => a.CollectionName == type.Name); return options != null; } - - } \ No newline at end of file diff --git a/src/AElf.EntityMapping.Elasticsearch/Services/IElasticIndexService.cs b/src/AElf.EntityMapping.Elasticsearch/Services/IElasticIndexService.cs index 6162f6dd..b4b9f702 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Services/IElasticIndexService.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Services/IElasticIndexService.cs @@ -2,11 +2,13 @@ namespace AElf.EntityMapping.Elasticsearch.Services; public interface IElasticIndexService { - Task CreateIndexAsync(string indexName, Type indexEntityType, int shard = 1, int numberOfReplicas = 1); + Task CreateIndexAsync(string indexName, Type indexEntityType, int shard = 1, int numberOfReplicas = 1, + Dictionary indexSettings = null); Task CreateIndexTemplateAsync(string indexTemplateName, string indexName, Type indexEntityType, int numberOfShards, int numberOfReplicas); Task CreateCollectionRouteKeyIndexAsync(Type indexEntityType, int numberOfShards, int numberOfReplicas); + Task DeleteIndexAsync(string collectionName = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/AElf.EntityMapping.Elasticsearch/Sharding/CollectionRouteKeyProvider.cs b/src/AElf.EntityMapping.Elasticsearch/Sharding/CollectionRouteKeyProvider.cs index 60e595ed..6615a9ad 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Sharding/CollectionRouteKeyProvider.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Sharding/CollectionRouteKeyProvider.cs @@ -110,14 +110,37 @@ public async Task> GetCollectionNameAsync(List(s => - s.Index(collectionRouteKeyIndexName).Size(10000).Query(q => q.Term(t => t.Field(f => f.CollectionRouteKey).Value(fieldValue))) - .Collapse(c => c.Field(f=>f.CollectionName)).Aggregations(a => a - .Cardinality("courseAgg", ca => ca.Field(f=>f.CollectionName)))); + s.Index(collectionRouteKeyIndexName) + .Query(q => q.Term(t => t.Field(f => f.CollectionRouteKey).Value(fieldValue))) + .Collapse(c => c.Field(f => f.CollectionName)) + .Size(1000)); + if (result == null) + { + _logger.LogError($"CollectionRouteKeyProvider.GetShardCollectionNameListByConditionsAsync: result is null fieldValue:{fieldValue}"); + } if (!result.IsValid) { - throw new ElasticsearchException($"Search document failed at index {collectionRouteKeyIndexName} :" + result.ServerError.Error.Reason); + if (result.ServerError == null || result.ServerError.Error == null || result.ServerError.Error.Reason == null) + { + _logger.LogError($"CollectionRouteKeyProvider.GetShardCollectionNameListByConditionsAsync: result.ServerError is null result:{JsonConvert.SerializeObject(result)}"); + } + var reason = result.ServerError?.Error?.Reason ?? "Unknown error"; + throw new ElasticsearchException($"Search document failed at index {collectionRouteKeyIndexName} :{reason}"); + } + + if (result.Documents == null) + { + _logger.LogError($"CollectionRouteKeyProvider.GetShardCollectionNameListByConditionsAsync: result.Documents is null fieldValue:{fieldValue}"); } var collectionList = result.Documents.ToList(); _logger.LogDebug($"CollectionRouteKeyProvider.GetShardCollectionNameListByConditionsAsync: " + diff --git a/src/AElf.EntityMapping.Elasticsearch/Sharding/ShardingKeyProvider.cs b/src/AElf.EntityMapping.Elasticsearch/Sharding/ShardingKeyProvider.cs index f9697e58..243d8ffe 100644 --- a/src/AElf.EntityMapping.Elasticsearch/Sharding/ShardingKeyProvider.cs +++ b/src/AElf.EntityMapping.Elasticsearch/Sharding/ShardingKeyProvider.cs @@ -70,8 +70,8 @@ public async Task> GetCollectionNameAsync(List(); } - List> shardingKeyInfos = GetShardKeyInfoList(); - List> filterShardingKeyInfos = shardingKeyInfos; + var shardingKeyInfos = GetShardKeyInfoList(); + var filterShardingKeyInfos = shardingKeyInfos; List filterConditions = new List(); foreach (var condition in conditions) diff --git a/src/AElf.EntityMapping/AElf.EntityMapping.csproj b/src/AElf.EntityMapping/AElf.EntityMapping.csproj index e60f85d4..21c00ccd 100644 --- a/src/AElf.EntityMapping/AElf.EntityMapping.csproj +++ b/src/AElf.EntityMapping/AElf.EntityMapping.csproj @@ -10,7 +10,9 @@ + + diff --git a/src/AElf.EntityMapping/Linq/AfterExpressionNode.cs b/src/AElf.EntityMapping/Linq/AfterExpressionNode.cs new file mode 100644 index 00000000..de8faab3 --- /dev/null +++ b/src/AElf.EntityMapping/Linq/AfterExpressionNode.cs @@ -0,0 +1,36 @@ +using System.Collections.ObjectModel; +using System.Linq.Expressions; +using System.Reflection; +using Remotion.Linq.Clauses; +using Remotion.Linq.Parsing.Structure.IntermediateModel; + +namespace AElf.EntityMapping.Linq; + +public class AfterExpressionNode : ResultOperatorExpressionNodeBase +{ + private static readonly IEnumerable SupportedMethods = + new ReadOnlyCollection((typeof(QueryableExtensions).GetRuntimeMethods()).ToList()) + .Where((Func)(mi => mi.Name == "After")); + + public static IEnumerable GetSupportedMethods() => SupportedMethods; + + public AfterExpressionNode(MethodCallExpressionParseInfo parseInfo, Expression position) + : base(parseInfo, null, null) + { + Position = position; + } + + public Expression Position { get; } + + public override Expression Resolve( + ParameterExpression inputParameter, Expression expressionToBeResolved, + ClauseGenerationContext clauseGenerationContext) + { + return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext); + } + + protected override ResultOperatorBase CreateResultOperator(ClauseGenerationContext clauseGenerationContext) + { + return new AfterResultOperator(Position); + } +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Linq/AfterResultOperator.cs b/src/AElf.EntityMapping/Linq/AfterResultOperator.cs new file mode 100644 index 00000000..7fbe609f --- /dev/null +++ b/src/AElf.EntityMapping/Linq/AfterResultOperator.cs @@ -0,0 +1,57 @@ +using System.Linq.Expressions; +using Remotion.Linq.Clauses; +using Remotion.Linq.Clauses.ResultOperators; +using Remotion.Linq.Clauses.StreamedData; + +namespace AElf.EntityMapping.Linq; + +public class AfterResultOperator : SequenceTypePreservingResultOperatorBase +{ + private Expression _position; + + public AfterResultOperator(Expression position) + { + Position = position; + } + + public Expression Position + { + get => _position; + set + { + if (value.Type != typeof(object[])) + { + var message = + $"The value expression returns '{value.Type}', an expression returning 'System.Object[]' was expected."; + throw new ArgumentException(message, nameof(value)); + } + + _position = value; + } + } + + public object[] GetConstantPosition() + { + return GetConstantValueFromExpression("position", Position); + } + + public override ResultOperatorBase Clone(CloneContext cloneContext) + { + return new AfterResultOperator(Position); + } + + public override void TransformExpressions(Func transformation) + { + Position = transformation(Position); + } + + public override string ToString() + { + return "After(" + string.Join(",", _position) + ")"; + } + + public override StreamedSequence ExecuteInMemory(StreamedSequence input) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Linq/EntityMappingCachedReflectionInfo.cs b/src/AElf.EntityMapping/Linq/EntityMappingCachedReflectionInfo.cs new file mode 100644 index 00000000..b39ead10 --- /dev/null +++ b/src/AElf.EntityMapping/Linq/EntityMappingCachedReflectionInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; + +namespace System.Linq; + +public static class EntityMappingCachedReflectionInfo +{ + private static MethodInfo? _afterMethodInfo; + + public static MethodInfo AfterMethodInfo(Type source) => + (_afterMethodInfo ??= new Func, object[], IQueryable>(QueryableExtensions.After).GetMethodInfo().GetGenericMethodDefinition()) + .MakeGenericMethod(source); +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Linq/EntityMappingEvaluatableExpressionFilter.cs b/src/AElf.EntityMapping/Linq/EntityMappingEvaluatableExpressionFilter.cs new file mode 100644 index 00000000..20d996fe --- /dev/null +++ b/src/AElf.EntityMapping/Linq/EntityMappingEvaluatableExpressionFilter.cs @@ -0,0 +1,7 @@ +using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; + +namespace AElf.EntityMapping.Linq; + +public class EntityMappingEvaluatableExpressionFilter : EvaluatableExpressionFilterBase +{ +} diff --git a/src/AElf.EntityMapping/Linq/EntityMappingNodeTypeProvider.cs b/src/AElf.EntityMapping/Linq/EntityMappingNodeTypeProvider.cs new file mode 100644 index 00000000..7264a160 --- /dev/null +++ b/src/AElf.EntityMapping/Linq/EntityMappingNodeTypeProvider.cs @@ -0,0 +1,135 @@ +using System.Reflection; +using Remotion.Linq.Parsing.Structure; + +namespace AElf.EntityMapping.Linq; + +public class EntityMappingNodeTypeProvider : INodeTypeProvider +{ + public static EntityMappingNodeTypeProvider Create() + { + var typeProvider = new EntityMappingNodeTypeProvider(); + typeProvider.Register(AfterExpressionNode.GetSupportedMethods(), typeof(AfterExpressionNode)); + return typeProvider; + } + + private static readonly Dictionary> GenericMethodDefinitionCandidates = new(); + + public void Register(IEnumerable methods, Type nodeType) + { + foreach (var method in methods) + { + if (method.IsGenericMethod && !method.IsGenericMethodDefinition) + throw new InvalidOperationException(string.Format( + "Cannot register closed generic method '{0}', try to register its generic method definition instead.", + new object[1] + { + (object)method.Name + })); + if (method.DeclaringType.GetTypeInfo().IsGenericType && + !method.DeclaringType.GetTypeInfo().IsGenericTypeDefinition) + throw new InvalidOperationException(string.Format( + "Cannot register method '{0}' in closed generic type '{1}', try to register its equivalent in the generic type definition instead.", + new object[2] + { + (object)method.Name, + (object)method.DeclaringType + })); + _registeredMethodInfoTypes[method] = nodeType; + } + } + + public bool IsRegistered(MethodInfo method) + { + return GetNodeType(method) != null; + } + + public Type GetNodeType(MethodInfo method) + { + var methodDefinition = GetRegisterableMethodDefinition(method, throwOnAmbiguousMatch: false); + if (methodDefinition == null) + return null; + + Type result; + _registeredMethodInfoTypes.TryGetValue(methodDefinition, out result); + return result; + } + + public static MethodInfo GetRegisterableMethodDefinition(MethodInfo method, bool throwOnAmbiguousMatch) + { + var genericMethodDefinition = method.IsGenericMethod ? method.GetGenericMethodDefinition() : method; + if (!genericMethodDefinition.DeclaringType.GetTypeInfo().IsGenericType) + return genericMethodDefinition; + + Lazy candidates; + lock (GenericMethodDefinitionCandidates) + { + if (!GenericMethodDefinitionCandidates.TryGetValue(method, out candidates)) + { + candidates = + new Lazy(() => GetGenericMethodDefinitionCandidates(genericMethodDefinition)); + GenericMethodDefinitionCandidates.Add(method, candidates); + } + } + + if (candidates.Value.Length == 1) + return candidates.Value.Single(); + + if (!throwOnAmbiguousMatch) + return null; + + throw new NotSupportedException( + string.Format( + "A generic method definition cannot be resolved for method '{0}' on type '{1}' because a distinct match is not possible. " + + @"The method can still be registered using the following syntax: + +public static readonly NameBasedRegistrationInfo[] SupportedMethodNames = + new[] {{ + new NameBasedRegistrationInfo ( + ""{2}"", + mi => /* match rule based on MethodInfo */ + ) + }};", + method, + genericMethodDefinition.DeclaringType.GetGenericTypeDefinition(), + method.Name)); + } + + private readonly Dictionary _registeredMethodInfoTypes = new Dictionary(); + + public int RegisteredMethodInfoCount => _registeredMethodInfoTypes.Count; + + private static MethodInfo[] GetGenericMethodDefinitionCandidates(MethodInfo referenceMethodDefinition) + { + var declaringTypeDefinition = referenceMethodDefinition.DeclaringType.GetGenericTypeDefinition(); + + var referenceMethodSignature = + new[] { new { Name = "returnValue", Type = referenceMethodDefinition.ReturnType } } + .Concat(referenceMethodDefinition.GetParameters() + .Select(p => new { Name = p.Name, Type = p.ParameterType })) + .ToArray(); + + var candidates = declaringTypeDefinition.GetRuntimeMethods() + .Select( + m => new + { + Method = m, + SignatureNames = new[] { "returnValue" }.Concat(m.GetParameters().Select(p => p.Name)).ToArray(), + SignatureTypes = new[] { m.ReturnType }.Concat(m.GetParameters().Select(p => p.ParameterType)) + .ToArray() + }) + .Where(c => c.Method.Name == referenceMethodDefinition.Name && + c.SignatureTypes.Length == referenceMethodSignature.Length) + .ToArray(); + + for (var i = 0; i < referenceMethodSignature.Length; i++) + { + candidates = candidates + .Where(c => c.SignatureNames[i] == referenceMethodSignature[i].Name) + .Where(c => c.SignatureTypes[i] == referenceMethodSignature[i].Type || + c.SignatureTypes[i].GetTypeInfo().ContainsGenericParameters) + .ToArray(); + } + + return candidates.Select(c => c.Method).ToArray(); + } +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Linq/QueryParserFactory.cs b/src/AElf.EntityMapping/Linq/QueryParserFactory.cs new file mode 100644 index 00000000..979bbc39 --- /dev/null +++ b/src/AElf.EntityMapping/Linq/QueryParserFactory.cs @@ -0,0 +1,27 @@ +using Remotion.Linq.Parsing.ExpressionVisitors.Transformation; +using Remotion.Linq.Parsing.Structure; +using Remotion.Linq.Parsing.Structure.NodeTypeProviders; + +namespace AElf.EntityMapping.Linq; + +public class QueryParserFactory +{ + public static QueryParser Create() + { + var transformerRegistry = ExpressionTransformerRegistry.CreateDefault(); + var evaluatableExpressionFilter = new EntityMappingEvaluatableExpressionFilter(); + + var innerProviders = new INodeTypeProvider[] + { + MethodInfoBasedNodeTypeRegistry.CreateFromRelinqAssembly(), + MethodNameBasedNodeTypeRegistry.CreateFromRelinqAssembly(), + EntityMappingNodeTypeProvider.Create() + }; + var nodeType = new CompoundNodeTypeProvider (innerProviders); + + var expressionTreeParser = new ExpressionTreeParser ( + nodeType, + ExpressionTreeParser.CreateDefaultProcessor (transformerRegistry, evaluatableExpressionFilter)); + return new QueryParser(expressionTreeParser); + } +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Linq/QueryableExtensions.cs b/src/AElf.EntityMapping/Linq/QueryableExtensions.cs new file mode 100644 index 00000000..c5058c05 --- /dev/null +++ b/src/AElf.EntityMapping/Linq/QueryableExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; + +namespace System.Linq; + +public static class QueryableExtensions +{ + public static IQueryable After(this IQueryable source, object[] position) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(position); + + return source.Provider.CreateQuery( + Expression.Call( + null, + EntityMappingCachedReflectionInfo.AfterMethodInfo(typeof(TSource)), + source.Expression, Expression.Constant(position) + )); + } +} \ No newline at end of file diff --git a/src/AElf.EntityMapping/Repositories/IEntityMappingBasicRepository.cs b/src/AElf.EntityMapping/Repositories/IEntityMappingBasicRepository.cs index a3db3510..fdea54c1 100644 --- a/src/AElf.EntityMapping/Repositories/IEntityMappingBasicRepository.cs +++ b/src/AElf.EntityMapping/Repositories/IEntityMappingBasicRepository.cs @@ -19,5 +19,7 @@ public interface IEntityMappingBasicRepository where TEntity : cl Task DeleteAsync(TEntity model, string collectionName = null, CancellationToken cancellationToken = default); Task DeleteManyAsync(List list, string collectionName = null, CancellationToken cancellationToken = default); + + } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/AElfElasticsearchTestsModule.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/AElfElasticsearchTestsModule.cs index 596c4ce8..2a4d9dd0 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/AElfElasticsearchTestsModule.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/AElfElasticsearchTestsModule.cs @@ -1,4 +1,5 @@ using AElf.EntityMapping.Elasticsearch.Options; +using AElf.EntityMapping.Elasticsearch.Services; using AElf.EntityMapping.Options; using AElf.EntityMapping.TestBase; using Elasticsearch.Net; @@ -6,6 +7,7 @@ using Microsoft.Extensions.Options; using Volo.Abp; using Volo.Abp.Modularity; +using Volo.Abp.Threading; namespace AElf.EntityMapping.Elasticsearch; @@ -36,9 +38,11 @@ public override void OnApplicationShutdown(ApplicationShutdownContext context) var clientProvider = context.ServiceProvider.GetRequiredService(); var client = clientProvider.GetClient(); + var elasticIndexService = context.ServiceProvider.GetRequiredService(); var indexPrefix = option.Value.CollectionPrefix.ToLower(); - client.Indices.Delete(indexPrefix+"*"); + // client.Indices.Delete(indexPrefix+"*"); + AsyncHelper.RunSync(async () => await elasticIndexService.DeleteIndexAsync(indexPrefix+"*")); client.Indices.DeleteTemplate(indexPrefix + "*"); } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/ElasticsearchCollectionNameProviderTests.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/ElasticsearchCollectionNameProviderTests.cs index 532bc492..fcbc73ef 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/ElasticsearchCollectionNameProviderTests.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/ElasticsearchCollectionNameProviderTests.cs @@ -72,7 +72,7 @@ public async Task GetCollectionName_Test() Type = ConditionType.Equal }); collectionNames = await _collectionNameProvider.GetFullCollectionNameAsync(collectionNameCondition); - collectionNames.Count.ShouldBe(0); + collectionNames.Count.ShouldBe(1); } [Fact] diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AccountBalanceEntity.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AccountBalanceEntity.cs new file mode 100644 index 00000000..106176b0 --- /dev/null +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AccountBalanceEntity.cs @@ -0,0 +1,13 @@ +using AElf.EntityMapping.Entities; +using Nest; + +namespace AElf.EntityMapping.Elasticsearch.Entities; + +public class AccountBalanceEntity: AeFinderEntity, IEntityMappingEntity +{ + [Keyword]public override string Id { get; set; } + public Metadata Metadata { get; set; } = new (); + [Keyword] public string Account { get; set; } + [Keyword] public string Symbol { get; set; } + public long Amount { get; set; } +} \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AeFinderEntity.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AeFinderEntity.cs new file mode 100644 index 00000000..022c389b --- /dev/null +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/AeFinderEntity.cs @@ -0,0 +1,32 @@ +using AElf.EntityMapping.Entities; +using Nest; +using Volo.Abp.Domain.Entities; + +namespace AElf.EntityMapping.Elasticsearch.Entities; + +public abstract class AeFinderEntity:Entity,IEntity +{ + /// + public virtual TKey Id { get; set; } + + protected AeFinderEntity() + { + + } + + protected AeFinderEntity(TKey id) + { + Id = id; + } + + public override object[] GetKeys() + { + return new object[] {Id}; + } + + /// + public override string ToString() + { + return $"[ENTITY: {GetType().Name}] Id = {Id}"; + } +} \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockBase.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockBase.cs index 1b856208..6f36bb3e 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockBase.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockBase.cs @@ -3,22 +3,26 @@ namespace AElf.EntityMapping.Elasticsearch.Entities; -public class BlockBase: AElfIndexerEntity,IBlockchainData +public class BlockBase : AElfIndexerEntity, IBlockchainData { - [Keyword]public override string Id { get; set; } + [Keyword] public override string Id { get; set; } + [Keyword] - [ShardPropertyAttributes("ChainId",1)] + [ShardPropertyAttributes("ChainId", 1)] public string ChainId { get; set; } - [CollectionRouteKey] - [Keyword]public string BlockHash { get; set; } - [ShardPropertyAttributes("BlockHeight",3)] + + [CollectionRouteKey] [Keyword] public string BlockHash { get; set; } + + [ShardPropertyAttributes("BlockHeight", 3)] public long BlockHeight { get; set; } - [Keyword]public string PreviousBlockHash { get; set; } + + [Keyword] public string PreviousBlockHash { get; set; } public DateTime BlockTime { get; set; } - [Keyword]public string SignerPubkey { get; set; } - [Keyword]public string Signature { get; set; } - [ShardPropertyAttributes("Confirmed",2)] - public bool Confirmed{get;set;} - public Dictionary ExtraProperties {get;set;} + [Keyword] public string SignerPubkey { get; set; } + [Keyword] public string Signature { get; set; } + + [ShardPropertyAttributes("Confirmed", 2)] + public bool Confirmed { get; set; } + public Dictionary ExtraProperties { get; set; } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockIndex.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockIndex.cs index cd28db54..02982609 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockIndex.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/BlockIndex.cs @@ -1,9 +1,18 @@ using AElf.EntityMapping.Entities; +using Nest; namespace AElf.EntityMapping.Elasticsearch.Entities; -public class BlockIndex:BlockBase,IEntityMappingEntity +public class BlockIndex : BlockBase, IEntityMappingEntity { public List TransactionIds { get; set; } public int LogEventCount { get; set; } + [Keyword] public string TxnFee { get; set; } + public FeeIndex Fee { get; set; } +} + +public class FeeIndex +{ + [Keyword] public string BlockFee { get; set; } + public long Fee { get; set; } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEvent.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEvent.cs index c4a2b763..7a503a64 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEvent.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEvent.cs @@ -3,12 +3,10 @@ namespace AElf.EntityMapping.Elasticsearch.Entities { - [NestedAttributes("LogEvents")] public class LogEvent : IBlockchainData { - [Keyword] - public string ChainId { get; set; } + [Keyword] public string ChainId { get; set; } [Keyword] public string BlockHash { get; set; } @@ -31,5 +29,4 @@ public class LogEvent : IBlockchainData public Dictionary ExtraProperties { get; set; } } - } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEventIndex.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEventIndex.cs index b5f4a2ac..038c6e0e 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEventIndex.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/LogEventIndex.cs @@ -4,40 +4,41 @@ namespace AElf.EntityMapping.Elasticsearch.Entities; -public class LogEventIndex:AElfIndexerEntity,IEntityMappingEntity,IBlockchainData +public class LogEventIndex : AElfIndexerEntity, IEntityMappingEntity, IBlockchainData { [Keyword] public override string Id { - get - { - return BlockHash + "_" + TransactionId + "_" + Index; - } + get { return BlockHash + "_" + TransactionId + "_" + Index; } } - [ShardPropertyAttributes("ChainId",1)] - [Keyword]public string ChainId { get; set; } - [Keyword]public string BlockHash { get; set; } - [Keyword]public string PreviousBlockHash { get; set; } + + [ShardPropertyAttributes("ChainId", 1)] + [Keyword] + public string ChainId { get; set; } + + [Keyword] public string BlockHash { get; set; } + [Keyword] public string PreviousBlockHash { get; set; } + /// /// block height /// - [ShardPropertyAttributes("BlockHeight",2)] + [ShardPropertyAttributes("BlockHeight", 2)] public long BlockHeight { get; set; } - - [Keyword]public string TransactionId { get; set; } - + + [Keyword] public string TransactionId { get; set; } + public DateTime BlockTime { get; set; } - - [Keyword]public string ContractAddress { get; set; } - - [Keyword]public string EventName { get; set; } - + + [Keyword] public string ContractAddress { get; set; } + + [Keyword] public string EventName { get; set; } + /// /// The ranking position of the event within the transaction /// public int Index { get; set; } - - public bool Confirmed{get;set;} - - public Dictionary ExtraProperties {get;set;} + + public bool Confirmed { get; set; } + + public Dictionary ExtraProperties { get; set; } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/Metadata.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/Metadata.cs new file mode 100644 index 00000000..10692424 --- /dev/null +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Entities/Metadata.cs @@ -0,0 +1,19 @@ +using Nest; + +namespace AElf.EntityMapping.Elasticsearch.Entities; + +public class Metadata +{ + [Keyword] + public string ChainId { get; set; } + public BlockMetadata Block { get; set; } + public bool IsDeleted { get; set; } +} + +public class BlockMetadata +{ + [Keyword] + public string BlockHash { get; set; } + public long BlockHeight { get; set; } + public DateTime BlockTime { get; set; } +} \ No newline at end of file diff --git a/test/AElf.EntityMapping.Elasticsearch.Tests/Repositories/ElasticsearchRepositoryTests.cs b/test/AElf.EntityMapping.Elasticsearch.Tests/Repositories/ElasticsearchRepositoryTests.cs index 8a876193..90cbe5e9 100644 --- a/test/AElf.EntityMapping.Elasticsearch.Tests/Repositories/ElasticsearchRepositoryTests.cs +++ b/test/AElf.EntityMapping.Elasticsearch.Tests/Repositories/ElasticsearchRepositoryTests.cs @@ -1,4 +1,6 @@ +using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Transactions; using AElf.EntityMapping.Elasticsearch.Entities; using AElf.EntityMapping.Elasticsearch.Services; using AElf.EntityMapping.Options; @@ -11,12 +13,16 @@ namespace AElf.EntityMapping.Elasticsearch.Repositories; public class ElasticsearchRepositoryTests : AElfElasticsearchTestBase { private readonly IElasticsearchRepository _elasticsearchRepository; + private readonly IElasticsearchRepository _transactionIndexRepository; + private readonly IElasticsearchRepository _accountBalanceRepository; private readonly IElasticIndexService _elasticIndexService; private readonly AElfEntityMappingOptions _option; public ElasticsearchRepositoryTests() { _elasticsearchRepository = GetRequiredService>(); + _transactionIndexRepository = GetRequiredService>(); + _accountBalanceRepository = GetRequiredService>(); _elasticIndexService = GetRequiredService(); _option = GetRequiredService>().Value; } @@ -24,7 +30,7 @@ public ElasticsearchRepositoryTests() [Fact] public async Task AddAsync() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -34,7 +40,7 @@ public async Task AddAsync() ChainId = "AELF" }; await _elasticsearchRepository.AddAsync(blockIndex); - + var queryable = await _elasticsearchRepository.GetQueryableAsync(); Expression> expression = p => p.ChainId == blockIndex.ChainId && p.BlockHeight == blockIndex.BlockHeight; @@ -43,13 +49,12 @@ public async Task AddAsync() Assert.True(results.First().Id == blockIndex.Id); Assert.True(results.First().BlockHeight == blockIndex.BlockHeight); await _elasticsearchRepository.DeleteAsync(blockIndex); - } - + [Fact] public async Task AddOrUpdateAsyncTest() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -59,7 +64,7 @@ public async Task AddOrUpdateAsyncTest() ChainId = "AELF" }; await _elasticsearchRepository.AddOrUpdateAsync(blockIndex); - + var queryable = await _elasticsearchRepository.GetQueryableAsync(); Expression> expression = p => p.ChainId == blockIndex.ChainId && p.BlockHeight == blockIndex.BlockHeight && p.Id == blockIndex.Id; @@ -68,7 +73,7 @@ public async Task AddOrUpdateAsyncTest() Assert.True(results.First().Id == blockIndex.Id); Assert.True(results.First().BlockHeight == blockIndex.BlockHeight); - var blockIndex2 = new BlockIndex + var blockIndex2 = new BlockIndex { Id = "block002", BlockHash = "BlockHash001", @@ -78,7 +83,7 @@ public async Task AddOrUpdateAsyncTest() ChainId = "AELF" }; await _elasticsearchRepository.AddOrUpdateAsync(blockIndex2); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex.ChainId && p.BlockHeight == blockIndex.BlockHeight && p.Id == blockIndex2.Id; @@ -86,7 +91,7 @@ public async Task AddOrUpdateAsyncTest() Assert.True(!results.IsNullOrEmpty()); Assert.True(results.First().Id == blockIndex2.Id); Assert.True(results.First().BlockHeight == blockIndex2.BlockHeight); - + expression = p => p.ChainId == blockIndex.ChainId && p.BlockHeight == blockIndex.BlockHeight; results = queryable.Where(expression).ToList(); @@ -95,11 +100,11 @@ public async Task AddOrUpdateAsyncTest() await _elasticsearchRepository.DeleteAsync(blockIndex); await _elasticsearchRepository.DeleteAsync(blockIndex2); } - + [Fact] public async Task AddOrUpdateManyAsyncTest() { - var blockIndex1 = new BlockIndex + var blockIndex1 = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -108,7 +113,7 @@ public async Task AddOrUpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var blockIndex2 = new BlockIndex + var blockIndex2 = new BlockIndex { Id = "block002", BlockHash = "BlockHash002", @@ -117,7 +122,7 @@ public async Task AddOrUpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var blockIndex3 = new BlockIndex + var blockIndex3 = new BlockIndex { Id = "block003", BlockHash = "BlockHash003", @@ -126,23 +131,23 @@ public async Task AddOrUpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var bulkList = new List {blockIndex1, blockIndex2, blockIndex3}; + var bulkList = new List { blockIndex1, blockIndex2, blockIndex3 }; await _elasticsearchRepository.AddOrUpdateManyAsync(bulkList); - + var queryable = await _elasticsearchRepository.GetQueryableAsync(); Expression> expression = p => p.ChainId == blockIndex1.ChainId && p.BlockHeight == blockIndex1.BlockHeight && p.Id == blockIndex1.Id; var results = queryable.Where(expression).ToList(); Assert.True(!results.IsNullOrEmpty()); Assert.True(results.First().Id == blockIndex1.Id); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex2.ChainId && p.BlockHeight == blockIndex2.BlockHeight && p.Id == blockIndex2.Id; results = queryable.Where(expression).ToList(); Assert.True(!results.IsNullOrEmpty()); Assert.True(results.First().Id == blockIndex2.Id); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex3.ChainId && p.BlockHeight == blockIndex3.BlockHeight && p.Id == blockIndex3.Id; @@ -153,11 +158,11 @@ public async Task AddOrUpdateManyAsyncTest() await _elasticsearchRepository.DeleteAsync(blockIndex2); await _elasticsearchRepository.DeleteAsync(blockIndex3); } - + [Fact] public async Task UpdateAsyncTest() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -169,7 +174,7 @@ public async Task UpdateAsyncTest() await _elasticsearchRepository.AddAsync(blockIndex); blockIndex.LogEventCount = 20; await _elasticsearchRepository.UpdateAsync(blockIndex); - + var queryable = await _elasticsearchRepository.GetQueryableAsync(); Expression> expression = p => p.ChainId == blockIndex.ChainId && p.Id == blockIndex.Id; @@ -183,7 +188,7 @@ public async Task UpdateAsyncTest() [Fact] public async Task UpdateManyAsyncTest() { - var blockIndex1 = new BlockIndex + var blockIndex1 = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -192,7 +197,7 @@ public async Task UpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var blockIndex2 = new BlockIndex + var blockIndex2 = new BlockIndex { Id = "block002", BlockHash = "BlockHash002", @@ -201,7 +206,7 @@ public async Task UpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var blockIndex3 = new BlockIndex + var blockIndex3 = new BlockIndex { Id = "block003", BlockHash = "BlockHash003", @@ -210,9 +215,9 @@ public async Task UpdateManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var bulkList = new List {blockIndex1, blockIndex2, blockIndex3}; + var bulkList = new List { blockIndex1, blockIndex2, blockIndex3 }; await _elasticsearchRepository.AddOrUpdateManyAsync(bulkList); - + var queryable = await _elasticsearchRepository.GetQueryableAsync(); Expression> expression = p => p.ChainId == blockIndex1.ChainId && p.BlockHeight == blockIndex1.BlockHeight && p.Id == blockIndex1.Id; @@ -220,7 +225,7 @@ public async Task UpdateManyAsyncTest() results.ShouldNotBeNull(); results.First().BlockHash.ShouldBe(blockIndex1.BlockHash); results.First().LogEventCount.ShouldBe(blockIndex1.LogEventCount); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex2.ChainId && p.BlockHeight == blockIndex2.BlockHeight && p.Id == blockIndex2.Id; @@ -228,7 +233,7 @@ public async Task UpdateManyAsyncTest() results.ShouldNotBeNull(); results.First().BlockHash.ShouldBe(blockIndex2.BlockHash); results.First().LogEventCount.ShouldBe(blockIndex2.LogEventCount); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex3.ChainId && p.BlockHeight == blockIndex3.BlockHeight && p.Id == blockIndex3.Id; @@ -236,27 +241,27 @@ public async Task UpdateManyAsyncTest() results.ShouldNotBeNull(); results.First().BlockHash.ShouldBe(blockIndex3.BlockHash); results.First().LogEventCount.ShouldBe(blockIndex3.LogEventCount); - + blockIndex1.LogEventCount = 100; blockIndex2.LogEventCount = 200; blockIndex3.LogEventCount = 300; - var bulkUpdateList = new List {blockIndex1, blockIndex2, blockIndex3}; + var bulkUpdateList = new List { blockIndex1, blockIndex2, blockIndex3 }; await _elasticsearchRepository.UpdateManyAsync(bulkUpdateList); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex1.ChainId && p.BlockHeight == blockIndex1.BlockHeight && p.Id == blockIndex1.Id; results = queryable.Where(expression).ToList(); results.ShouldNotBeNull(); results.First().LogEventCount.ShouldBe(100); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex2.ChainId && p.BlockHeight == blockIndex2.BlockHeight && p.Id == blockIndex2.Id; results = queryable.Where(expression).ToList(); results.ShouldNotBeNull(); results.First().LogEventCount.ShouldBe(200); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex3.ChainId && p.BlockHeight == blockIndex3.BlockHeight && p.Id == blockIndex3.Id; @@ -264,11 +269,11 @@ public async Task UpdateManyAsyncTest() results.ShouldNotBeNull(); results.First().LogEventCount.ShouldBe(300); } - + [Fact] public async Task DeleteAsyncByEntityTest() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -283,9 +288,9 @@ public async Task DeleteAsyncByEntityTest() p.ChainId == blockIndex.ChainId && p.Id == blockIndex.Id; var results = queryable.Where(expression).ToList(); Assert.True(!results.IsNullOrEmpty()); - + await _elasticsearchRepository.DeleteAsync(blockIndex); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex.ChainId && p.Id == blockIndex.Id; @@ -296,7 +301,7 @@ public async Task DeleteAsyncByEntityTest() [Fact] public async Task DeleteAsyncByIdTest() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -313,7 +318,7 @@ public async Task DeleteAsyncByIdTest() p.ChainId == blockIndex.ChainId && p.Id == blockIndex.Id; var results = queryable.Where(expression).ToList(); Assert.True(!results.IsNullOrEmpty()); - + await _elasticsearchRepository.DeleteAsync(blockIndex.Id); Thread.Sleep(1000); queryable = await _elasticsearchRepository.GetQueryableAsync(); @@ -322,11 +327,11 @@ public async Task DeleteAsyncByIdTest() results = queryable.Where(expression).ToList(); Assert.True(results.IsNullOrEmpty()); } - + [Fact] public async Task DeleteManyAsyncTest() { - var blockIndex1 = new BlockIndex + var blockIndex1 = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -335,7 +340,7 @@ public async Task DeleteManyAsyncTest() LogEventCount = 10, ChainId = "AELF" }; - var blockIndex2 = new BlockIndex + var blockIndex2 = new BlockIndex { Id = "block002", BlockHash = "BlockHash002", @@ -344,7 +349,7 @@ public async Task DeleteManyAsyncTest() LogEventCount = 20, ChainId = "AELF" }; - var blockIndex3 = new BlockIndex + var blockIndex3 = new BlockIndex { Id = "block003", BlockHash = "BlockHash003", @@ -353,7 +358,7 @@ public async Task DeleteManyAsyncTest() LogEventCount = 30, ChainId = "AELF" }; - var blockIndex30 = new BlockIndex + var blockIndex30 = new BlockIndex { Id = "block030", BlockHash = "BlockHash030", @@ -362,35 +367,35 @@ public async Task DeleteManyAsyncTest() LogEventCount = 30, ChainId = "AELF" }; - var bulkList = new List {blockIndex1, blockIndex2, blockIndex3,blockIndex30}; + var bulkList = new List { blockIndex1, blockIndex2, blockIndex3, blockIndex30 }; await _elasticsearchRepository.AddOrUpdateManyAsync(bulkList); await _elasticsearchRepository.DeleteManyAsync(bulkList); var queryable = await _elasticsearchRepository.GetQueryableAsync(); - + Expression> expression = p => p.ChainId == blockIndex1.ChainId && p.Id == blockIndex1.Id; Thread.Sleep(500); var results = queryable.Where(expression).ToList(); Assert.True(results.IsNullOrEmpty()); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex2.ChainId && p.Id == blockIndex2.Id; results = queryable.Where(expression).ToList(); Assert.True(results.IsNullOrEmpty()); - + queryable = await _elasticsearchRepository.GetQueryableAsync(); expression = p => p.ChainId == blockIndex30.ChainId && p.Id == blockIndex30.Id; results = queryable.Where(expression).ToList(); Assert.True(results.IsNullOrEmpty()); } - + [Fact] public async Task Get_Test() { - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -408,7 +413,7 @@ public async Task Get_Test() block.BlockHeight.ShouldBe(blockIndex.BlockHeight); block.LogEventCount.ShouldBe(blockIndex.LogEventCount); block.ChainId.ShouldBe(blockIndex.ChainId); - + for (int i = 1; i <= 7; i++) { await _elasticsearchRepository.AddAsync(new BlockIndex @@ -421,7 +426,7 @@ await _elasticsearchRepository.AddAsync(new BlockIndex ChainId = "AELF" }); } - + block = await _elasticsearchRepository.GetAsync("block7"); block.Id.ShouldBe("block7"); } @@ -430,7 +435,7 @@ await _elasticsearchRepository.AddAsync(new BlockIndex public async Task Get_SpecificIndex_Test() { var indexName = $"{_option.CollectionPrefix}.block".ToLower(); - var blockIndex = new BlockIndex + var blockIndex = new BlockIndex { Id = "block001", BlockHash = "BlockHash001", @@ -507,7 +512,7 @@ public async Task GetList_SpecificIndex_Test() { var indexName = $"{_option.CollectionPrefix}.block".ToLower(); await _elasticIndexService.CreateIndexAsync(indexName, typeof(BlockIndex), 1, 0); - + for (int i = 1; i <= 7; i++) { var blockIndex = new BlockIndex @@ -551,6 +556,81 @@ public async Task GetList_SpecificIndex_Test() list[0].BlockHeight.ShouldBe(6); } + private async Task ClearTransactionIndex(string chainId, long startBlockNumber, long endBlockNumber) + { + Expression> expression = p => p.ChainId == chainId && p.BlockHeight >= startBlockNumber && p.BlockHeight <= endBlockNumber; + var queryable = await _transactionIndexRepository.GetQueryableAsync(); + var filterList = queryable.Where(expression).ToList(); + foreach (var deleteTransaction in filterList) + { + await _transactionIndexRepository.DeleteAsync(deleteTransaction); + } + } + + [Fact] + public async Task GetList_Nested_Test() + { + //clear data for unit test + ClearTransactionIndex("AELF", 100, 110); + + Thread.Sleep(2000); + //Unit Test 14 + var transaction_100_1 = MockNewTransactionEtoData(100, false, "token_contract_address", "DonateResourceToken"); + var transaction_100_2 = MockNewTransactionEtoData(100, false, "", ""); + var transaction_100_3 = MockNewTransactionEtoData(100, false, "consensus_contract_address", "UpdateValue"); + var transaction_110 = MockNewTransactionEtoData(110, true, "consensus_contract_address", "UpdateTinyBlockInformation"); + await _transactionIndexRepository.AddAsync(transaction_100_1); + await _transactionIndexRepository.AddAsync(transaction_100_2); + await _transactionIndexRepository.AddAsync(transaction_100_3); + await _transactionIndexRepository.AddAsync(transaction_110); + Thread.Sleep(2000); + + var chainId = "AELF"; + Expression> mustQuery = p => p.LogEvents.Any(i => i.ChainId == chainId && i.BlockHeight >= 100 && i.BlockHeight <= 110); + mustQuery = p => p.LogEvents.Any(i => i.ChainId == "AELF" && i.TransactionId == transaction_100_1.TransactionId); + var queryable = await _transactionIndexRepository.GetQueryableAsync(); + var filterList = queryable.Where(mustQuery).ToList(); + filterList.Count.ShouldBe(1); + } + + [Fact] + public async Task SubObjectQueryTest() + { + for (int i = 1; i <= 7; i++) + { + var accountBalanceEntity = new AccountBalanceEntity + { + Id = "block" + i, + Account = "BlockHash" + i, + Amount = i, + Symbol = "AELF", + Metadata = new Metadata() + { + ChainId = "tDVV", + Block=new BlockMetadata() + { + BlockHash = "BlockHash" + i, + BlockHeight = i, + BlockTime = DateTime.Now.AddDays(-10 + i) + }, + IsDeleted=false + } + }; + await _accountBalanceRepository.AddAsync(accountBalanceEntity); + } + + var queryable = await _accountBalanceRepository.GetQueryableAsync(); + var list1 = queryable.Where(o => o.Metadata.Block.BlockHash == "BlockHash5").ToList(); + // var list1 = queryable.Where(o => o.Metadata.ChainId == "tDVV").ToList(); + // var list1 = queryable.Where(o => o.Account == "BlockHash3").ToList(); + list1.Count.ShouldBe(1); + + var list = await _accountBalanceRepository.GetListAsync(o=>o.Metadata.Block.BlockHash == "BlockHash4"); + // var list = await _accountBalanceRepository.GetListAsync(o => o.Account == "BlockHash3"); + list.Count.ShouldBe(1); + } + + [Fact] public async Task GetList_MultipleChain_Test() { @@ -563,11 +643,17 @@ public async Task GetList_MultipleChain_Test() BlockHeight = i, BlockTime = DateTime.Now.AddDays(-10 + i), LogEventCount = i, - ChainId = "AELF" + ChainId = "AELF", + TxnFee = "BlockHash" + i, + Fee = new FeeIndex() + { + BlockFee = "BlockHash" + i, + Fee = i % 4 + } }; await _elasticsearchRepository.AddAsync(blockIndex); } - + for (int i = 1; i <= 11; i++) { var blockIndex = new BlockIndex @@ -581,11 +667,229 @@ public async Task GetList_MultipleChain_Test() }; await _elasticsearchRepository.AddAsync(blockIndex); } + + var chainId = "AELF"; + var list = await _elasticsearchRepository.GetListAsync(o => o.ChainId == chainId && o.BlockHeight >= 0 && o.Fee.BlockFee == "BlockHash2"); + list.Count.ShouldBe(1); + foreach (var e in list) + { + e.ChainId.ShouldBe(chainId); + e.BlockHeight.ShouldBeGreaterThan(0); + e.Fee.BlockFee.ShouldBe("BlockHash2"); + } + + var queryable = await _elasticsearchRepository.GetQueryableAsync(); + var list1 = queryable.Where(o => o.ChainId == chainId).OrderByDescending(o => o.LogEventCount).ToList(); + list1.Count.ShouldBe(7); + list1.First().LogEventCount.ShouldBe(7); + foreach (var e in list1) + { + e.ChainId.ShouldBe(chainId); + } + + var list2 = queryable.Where(o => o.ChainId == chainId && o.BlockHeight >= 0 && o.Fee.Fee == 3).ToList(); + list2.Count.ShouldBe(2); + foreach (var e in list2) + { + e.ChainId.ShouldBe(chainId); + e.BlockHeight.ShouldBeGreaterThanOrEqualTo(0); + e.Fee.Fee.ShouldBe(3); + } + + var list3 = queryable.Where(o => o.ChainId == chainId && o.BlockHeight >= 2).OrderBy(o => o.Fee.Fee).ToList(); + list3.Count.ShouldBe(6); + list3.First().Fee.Fee.ShouldBe(0); + foreach (var e in list3) + { + e.ChainId.ShouldBe(chainId); + e.BlockHeight.ShouldBeGreaterThanOrEqualTo(2); + } + } + + [Fact] + public async Task GetList_WildCard_Test() + { + for (int i = 1; i <= 7; i++) + { + var blockIndex = new BlockIndex + { + Id = "block" + i, + BlockHash = "BlockHash" + i, + BlockHeight = i, + BlockTime = DateTime.Now.AddDays(-10 + i), + LogEventCount = i, + ChainId = "AELF", + TxnFee = "BlockHash" + i, + Fee = new FeeIndex() + { + BlockFee = "BlockHash" + i, + Fee = i % 4 + } + }; + await _elasticsearchRepository.AddAsync(blockIndex); + } + + for (int i = 1; i <= 11; i++) + { + var blockIndex = new BlockIndex + { + Id = "block" + i, + BlockHash = "BlockHash" + i, + BlockHeight = i, + BlockTime = DateTime.Now.AddDays(-10 + i), + LogEventCount = i, + ChainId = "tDVV" + }; + await _elasticsearchRepository.AddAsync(blockIndex); + } + + var queryable = await _elasticsearchRepository.GetQueryableAsync(); + var list4 = queryable.Where(o => o.ChainId == "AELF" && o.BlockHash.StartsWith("BlockHash")).ToList(); + list4.Count.ShouldBe(7); + + var list5 = queryable.Where(o => o.ChainId == "AELF" && o.Id.Contains("6")).ToList(); + list5.Count.ShouldBe(1); + list5.First().Id.ShouldBe("block6"); + + var list6 = queryable.Where(o => o.ChainId == "AELF" && o.BlockHash.EndsWith("7")).ToList(); + list6.Count.ShouldBe(1); + list6.First().Id.ShouldBe("block7"); + + var list7 = queryable.Where(o => o.ChainId == "AELF" && o.Fee.BlockFee.StartsWith("BlockHash")).ToList(); + list7.Count.ShouldBe(7); + + var list8 = queryable.Where(o => o.ChainId == "AELF" && o.Fee.BlockFee.Contains("6")).ToList(); + list8.Count.ShouldBe(1); + list8.First().Id.ShouldBe("block6"); + + var list9 = queryable.Where(o => o.ChainId == "AELF" && o.Fee.BlockFee.EndsWith("7")).ToList(); + list9.Count.ShouldBe(1); + list9.First().Id.ShouldBe("block7"); + } + + + public static TransactionIndex MockNewTransactionEtoData(long blockHeight, bool isConfirmed, string contractAddress, string eventName) + { + string currentBlockHash = CreateBlockHash(); + string transactionId = CreateBlockHash(); + var newTransaction = new TransactionIndex() + { + Id = transactionId, + TransactionId = transactionId, + ChainId = "AELF", + From = "2pL7foxBhMC1RVZMUEtkvYK4pWWaiLHBAQcXFdzfD5oZjYSr3e", + To = "pGa4e5hNGsgkfjEGm72TEvbF7aRDqKBd4LuXtab4ucMbXLcgJ", + BlockHash = currentBlockHash, + BlockHeight = blockHeight, + BlockTime = DateTime.Now, + MethodName = "UpdateValue", + Params = + "CiIKINnB4HhmTpMScNl9T4hNoR1w8dOpx0O684p+pwfm6uOkEiIKINZUk1v+szUqMZZ3w0phLn7qqOX+h+uD1fdP59LYm9CsIiIKIPGQ8ga2zO3CXXtZkjlMma6CI7VSQFA7ZMYrIU6zGT/uKgYInMjcmAYwAVDPAlqpAQqCATA0YmNkMWM4ODdjZDBlZGJkNGNjZjhkOWQyYjNmNzJlNzI1MTFhYTYxODMxOTk2MDAzMTM2ODdiYTZjNTgzZjEzYzNkNmQ3MTZmYTQwZGY4NjA0YWFlZDBmY2FiMzExMzVmZTNjMmQ0NWMwMDk4MDBjMDc1MjU0YTM3ODJiNGM0ZGISIgog8ZDyBrbM7cJde1mSOUyZroIjtVJAUDtkxishTrMZP+5g0AI=", + Signature = + "1KblGpvuuo+HSDdh0OhRq/vg3Ts4HoqcIwBeni/356pdEbgnnR2yqbpgvzNs+oNeBb4Ux2kE1XY9lk+p60LfWgA=", + Index = 0, + Status = TransactionStatus.Committed, + Confirmed = isConfirmed, + ExtraProperties = new Dictionary() + { + ["Version"] = "0", + ["RefBlockNumber"] = "335", + ["RefBlockPrefix"] = "156ff372", + ["Bloom"] = + "AAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAAAAAgAAABAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgAAAAAAAAAABAAAAAAAAAAAAAAIQAAAAABAAAAAgAAAAAAAAAAAAAAAAABACAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAAAAAAAAAAAAAAEAABAAAAAAAAAAAAAAAAAAAAgAAAAgAAAAABAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAgAAAAAAAAAAAAAA==", + ["ReturnValue"] = "", + ["Error"] = "" + }, + LogEvents = new List() + { + new LogEvent() + { + ChainId = "AELF", + BlockHash = currentBlockHash, + BlockHeight = blockHeight, + BlockTime = DateTime.Now, + Confirmed = isConfirmed, + TransactionId = transactionId, + ContractAddress = contractAddress, + EventName = eventName, + Index = 0, + ExtraProperties = new Dictionary() + { + ["Indexed"] = + "[ \"CoIBMDRiY2QxYzg4N2NkMGVkYmQ0Y2NmOGQ5ZDJiM2Y3MmU3MjUxMWFhNjE4MzE5OTYwMDMxMzY4N2JhNmM1ODNmMTNjM2Q2ZDcxNmZhNDBkZjg2MDRhYWVkMGZjYWIzMTEzNWZlM2MyZDQ1YzAwOTgwMGMwNzUyNTRhMzc4MmI0YzRkYg==\", \"EgsIxNqlmQYQ4M74LQ==\", \"GhpVcGRhdGVUaW55QmxvY2tJbmZvcm1hdGlvbg==\", \"ILAi\", \"KiIKID3kBhYftHeFZBYS6VOXPeigGAAwZWM85SlzN48xJARW\" ]", + ["NonIndexed"] = "" + } + } + } + }; + return newTransaction; + } + + public static string CreateBlockHash() + { + return Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); + } + + [Fact] + public async Task After_Test() + { + var blockIndex = new BlockIndex + { + Id = "block010", + BlockHash = "BlockHash", + BlockHeight = 10, + BlockTime = DateTime.Now, + ChainId = "AELF" + }; + await _elasticsearchRepository.AddAsync(blockIndex); - var list = await _elasticsearchRepository.GetListAsync(o =>o.ChainId =="AELF" && o.BlockHeight >= 0); - list.Count.ShouldBe(7); + blockIndex = new BlockIndex + { + Id = "block005", + BlockHash = "BlockHash", + BlockHeight = 10, + BlockTime = DateTime.Now, + ChainId = "AELF" + }; + await _elasticsearchRepository.AddAsync(blockIndex); + + blockIndex = new BlockIndex + { + Id = "block009", + BlockHash = "BlockHash", + BlockHeight = 9, + BlockTime = DateTime.Now, + ChainId = "AELF" + }; + await _elasticsearchRepository.AddAsync(blockIndex); - list = await _elasticsearchRepository.GetListAsync(o =>o.ChainId =="tDVV" && o.BlockHeight >= 0); - list.Count.ShouldBe(11); + blockIndex = new BlockIndex + { + Id = "block008", + BlockHash = "BlockHash", + BlockHeight = 12, + BlockTime = DateTime.Now, + ChainId = "AELF" + }; + await _elasticsearchRepository.AddAsync(blockIndex); + + blockIndex = new BlockIndex + { + Id = "block001", + BlockHash = "BlockHash", + BlockHeight = 1, + BlockTime = DateTime.Now, + ChainId = "AELF" + }; + await _elasticsearchRepository.AddAsync(blockIndex); + + + var queryable = await _elasticsearchRepository.GetQueryableAsync(); + queryable = queryable.Where(o => o.ChainId == "AELF" && o.BlockHeight >= 1).OrderBy(o=>o.BlockHeight).OrderBy(o=>o.Id).After(new object[]{9,"block009"}); + var list = queryable.ToList(); + list.Count.ShouldBe(3); + list[0].Id.ShouldBe("block005"); + list[1].Id.ShouldBe("block010"); + list[2].Id.ShouldBe("block008"); } } \ No newline at end of file diff --git a/test/AElf.EntityMapping.Tests/AElf.EntityMapping.Tests.csproj b/test/AElf.EntityMapping.Tests/AElf.EntityMapping.Tests.csproj index c9ca4a06..7efad299 100644 --- a/test/AElf.EntityMapping.Tests/AElf.EntityMapping.Tests.csproj +++ b/test/AElf.EntityMapping.Tests/AElf.EntityMapping.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/test/AElf.EntityMapping.Tests/AElfEntityMappingTestModule.cs b/test/AElf.EntityMapping.Tests/AElfEntityMappingTestModule.cs index a3d01f2d..0897b43c 100644 --- a/test/AElf.EntityMapping.Tests/AElfEntityMappingTestModule.cs +++ b/test/AElf.EntityMapping.Tests/AElfEntityMappingTestModule.cs @@ -1,3 +1,4 @@ +using AElf.EntityMapping.Elasticsearch.Options; using AElf.EntityMapping.Options; using AElf.EntityMapping.Sharding; using AElf.EntityMapping.TestBase; @@ -20,6 +21,10 @@ public override void ConfigureServices(ServiceConfigurationContext context) { x.AddModule(typeof(AElfEntityMappingTestModule)); }); + Configure(x => + { + x.Uris = new List {"http://localhost:9200"}; + }); context.Services.Configure(options => { @@ -31,93 +36,97 @@ public override void ConfigureServices(ServiceConfigurationContext context) private List InitShardInitSettingOptions() { - ShardInitSetting blockIndexDto = new ShardInitSetting(); - blockIndexDto.CollectionName = "BlockIndex"; - blockIndexDto.ShardGroups = new List() + var blockIndexDto = new ShardInitSetting { - new ShardGroup() + CollectionName = "BlockIndex", + ShardGroups = new List() { - ShardKeys = new List() + new ShardGroup() { - new ShardKey() - { - Name = "ChainId", - Value = "AELF", - Step = "", - StepType = StepType.None - }, - new ShardKey() + ShardKeys = new List() { - Name = "BlockHeight", - Value = "0", - Step = "5", - StepType = StepType.Floor + new ShardKey() + { + Name = "ChainId", + Value = "AELF", + Step = "", + StepType = StepType.None + }, + new ShardKey() + { + Name = "BlockHeight", + Value = "0", + Step = "5", + StepType = StepType.Floor + } } - } - }, - new ShardGroup() - { - ShardKeys = new List() + }, + new ShardGroup() { - new ShardKey() + ShardKeys = new List() { - Name = "ChainId", - Value = "tDVV", - Step = "", - StepType = StepType.None - }, - new ShardKey() - { - Name = "BlockHeight", - Value = "0", - Step = "10", - StepType = StepType.Floor + new ShardKey() + { + Name = "ChainId", + Value = "tDVV", + Step = "", + StepType = StepType.None + }, + new ShardKey() + { + Name = "BlockHeight", + Value = "0", + Step = "10", + StepType = StepType.Floor + } } } } }; - ShardInitSetting logEventIndexDto = new ShardInitSetting(); - logEventIndexDto.CollectionName = "LogEventIndex"; - logEventIndexDto.ShardGroups = new List() + var logEventIndexDto = new ShardInitSetting { - new ShardGroup() + CollectionName = "LogEventIndex", + ShardGroups = new List() { - ShardKeys = new List() + new ShardGroup() { - new ShardKey() - { - Name = "ChainId", - Value = "AELF", - Step = "", - StepType = StepType.None - }, - new ShardKey() + ShardKeys = new List() { - Name = "BlockHeight", - Value = "0", - Step = "2000", - StepType = StepType.Floor + new ShardKey() + { + Name = "ChainId", + Value = "AELF", + Step = "", + StepType = StepType.None + }, + new ShardKey() + { + Name = "BlockHeight", + Value = "0", + Step = "2000", + StepType = StepType.Floor + } } - } - }, - new ShardGroup() - { - ShardKeys = new List() + }, + new ShardGroup() { - new ShardKey() - { - Name = "ChainId", - Value = "tDVV", - Step = "", - StepType = StepType.None - }, - new ShardKey() + ShardKeys = new List() { - Name = "BlockHeight", - Value = "0", - Step = "1000", - StepType = StepType.Floor + new ShardKey() + { + Name = "ChainId", + Value = "tDVV", + Step = "", + StepType = StepType.None + }, + new ShardKey() + { + Name = "BlockHeight", + Value = "0", + Step = "1000", + StepType = StepType.Floor + } } } }