From ff4fe02e4588571120caec526a0055522b19ea02 Mon Sep 17 00:00:00 2001 From: Dmytro Honcharenko Date: Fri, 23 Jun 2023 13:06:20 +0300 Subject: [PATCH 1/9] {{wip}} --- .../Events/Instance/CategoryCreated.cs | 30 +++++++++++++++++++ .../Entity/EntityCategoryPathChanged.cs | 10 ++++++- .../ValueFromListOptionConfiguration.cs | 6 ++-- CloudFabric.EAV.Domain/Models/Category.cs | 14 +++++++-- CloudFabric.EAV.Domain/Models/CategoryPath.cs | 2 ++ .../Models/EntityInstanceBase.cs | 12 ++++++-- .../EntityInstanceProjectionBuilder.cs | 10 +++++-- .../Utilities/Extensions/StringExtensions.cs | 12 ++++++++ .../CategoryInstanceCreateRequest.cs | 1 + CloudFabric.EAV.Service/EAVService.cs | 17 ++++++----- .../CategoryTests/CategoryTests.cs | 2 +- 11 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs create mode 100644 CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs diff --git a/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs new file mode 100644 index 0000000..9ab9a70 --- /dev/null +++ b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs @@ -0,0 +1,30 @@ +using CloudFabric.EventSourcing.EventStore; + +namespace CloudFabric.EAV.Domain.Models; + +public record CategoryCreated : Event +{ + public CategoryCreated() + { + + } + + public CategoryCreated(Guid id, + string machineName, + Guid entityConfigurationId, + List attributes, + Guid? tenantId) + { + TenantId = tenantId; + Attributes = attributes; + EntityConfigurationId = entityConfigurationId; + AggregateId = id; + MachineName = machineName; + } + + public Guid EntityConfigurationId { get; set; } + public List Attributes { get; set; } + public Guid? TenantId { get; set; } + public string MachineName { get; set; } + +} diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs index 990ca06..85a38ea 100644 --- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs @@ -10,16 +10,24 @@ public EntityCategoryPathChanged() { } - public EntityCategoryPathChanged(Guid id, Guid entityConfigurationId, Guid categoryTreeId, string categoryPath) + public EntityCategoryPathChanged(Guid id, + Guid entityConfigurationId, + Guid categoryTreeId, + string categoryPath, + Guid parentId) { AggregateId = id; EntityConfigurationId = entityConfigurationId; CategoryPath = categoryPath; CategoryTreeId = categoryTreeId; + ParentId = parentId; + ParentMachineName = categoryPath.Split('/').Last(x => !string.IsNullOrEmpty(x)); } public string CategoryPath { get; set; } public Guid EntityConfigurationId { get; set; } public Guid CategoryTreeId { get; set; } + public Guid ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs index 1c1afc8..ba5eb31 100644 --- a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs +++ b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; +using CloudFabric.EAV.Domain.Utilities.Extensions; + namespace CloudFabric.EAV.Domain.Models.Attributes; public class ValueFromListOptionConfiguration @@ -11,9 +13,7 @@ public ValueFromListOptionConfiguration(string name, string? machineName) if (string.IsNullOrEmpty(machineName)) { - machineName = name.Replace(" ", "_"); - var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100)); - machineName = specSymbolsRegex.Replace(machineName, "").ToLower(); + machineName = name.SanitizeForMachineName(); } MachineName = machineName; diff --git a/CloudFabric.EAV.Domain/Models/Category.cs b/CloudFabric.EAV.Domain/Models/Category.cs index 8d9f539..a3f6f87 100644 --- a/CloudFabric.EAV.Domain/Models/Category.cs +++ b/CloudFabric.EAV.Domain/Models/Category.cs @@ -5,24 +5,32 @@ namespace CloudFabric.EAV.Domain.Models; public class Category : EntityInstanceBase { + public string MachineName { get; set; } public Category(IEnumerable events) : base(events) { } - public Category(Guid id, Guid entityConfigurationId, List attributes, Guid? tenantId) + public Category(Guid id, + string machineName, + Guid entityConfigurationId, + List attributes, + Guid? tenantId) : base(id, entityConfigurationId, attributes, tenantId) { + Apply(new CategoryCreated(id, machineName, entityConfigurationId, attributes, tenantId)); } public Category( Guid id, + string machineName, Guid entityConfigurationId, List attributes, Guid? tenantId, string categoryPath, + Guid parentId, Guid categoryTreeId - ) : base(id, entityConfigurationId, attributes, tenantId) + ) : this(id, machineName, entityConfigurationId, attributes, tenantId) { - Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath)); + Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath, parentId)); } } diff --git a/CloudFabric.EAV.Domain/Models/CategoryPath.cs b/CloudFabric.EAV.Domain/Models/CategoryPath.cs index 608929e..30d3d3b 100644 --- a/CloudFabric.EAV.Domain/Models/CategoryPath.cs +++ b/CloudFabric.EAV.Domain/Models/CategoryPath.cs @@ -4,4 +4,6 @@ public class CategoryPath { public Guid TreeId { get; set; } public string Path { get; set; } + public Guid ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs index 99f78a7..250f803 100644 --- a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs +++ b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs @@ -54,9 +54,9 @@ public void On(EntityInstanceCreated @event) CategoryPaths = new List(); } - public void ChangeCategoryPath(Guid treeId, string categoryPath) + public void ChangeCategoryPath(Guid treeId, string categoryPath, Guid parentId) { - Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath)); + Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath, parentId)); } public void On(EntityCategoryPathChanged @event) @@ -64,11 +64,17 @@ public void On(EntityCategoryPathChanged @event) CategoryPath? categoryPath = CategoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId); if (categoryPath == null) { - CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, Path = @event.CategoryPath }); + CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, + Path = @event.CategoryPath, + ParentId = @event.ParentId, + ParentMachineName = @event.ParentMachineName + }); } else { categoryPath.Path = @event.CategoryPath; + categoryPath.ParentMachineName = @event.ParentMachineName; + categoryPath.ParentId = @event.ParentId; } } diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs index 27da37e..27ec493 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs @@ -134,14 +134,20 @@ await UpdateDocument( List categoryPaths = categoryPathsObj as List ?? new List(); CategoryPath? categoryPath = categoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId); + if (categoryPath == null) { - categoryPaths.Add(new CategoryPath { Path = @event.CategoryPath, TreeId = @event.CategoryTreeId } - ); + categoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, + Path = @event.CategoryPath, + ParentId = @event.ParentId, + ParentMachineName = @event.ParentMachineName + }); } else { categoryPath.Path = @event.CategoryPath; + categoryPath.ParentMachineName = @event.ParentMachineName; + categoryPath.ParentId = @event.ParentId; } document["CategoryPaths"] = categoryPaths; diff --git a/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs new file mode 100644 index 0000000..6adf242 --- /dev/null +++ b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +using System.Text.RegularExpressions; + +namespace CloudFabric.EAV.Domain.Utilities.Extensions; + +public static class StringExtensions +{ + public static string SanitizeForMachineName(this string str) + { + var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100)); + return specSymbolsRegex.Replace(str.Replace(" ", "_"), "").ToLower(); + } +} diff --git a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs index 4db28c8..86f1f55 100644 --- a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs @@ -6,6 +6,7 @@ public class CategoryInstanceCreateRequest public Guid CategoryTreeId { get; set; } + public string MachineName { get; set; } public List Attributes { get; set; } public Guid? ParentId { get; set; } diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 5e78527..8f96e0c 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -271,14 +271,14 @@ private async Task BuildCategoryPath(Guid treeId, Guid? parentId, + private async Task<(string?, Guid?, ProblemDetails?)> BuildCategoryPath(Guid treeId, Guid? parentId, CancellationToken cancellationToken) { CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) .ConfigureAwait(false); if (tree == null) { - return (null, new ValidationErrorResponse("TreeId", "Tree not found"))!; + return (null, null, new ValidationErrorResponse("TreeId", "Tree not found"))!; } Category? parent = parentId == null @@ -290,12 +290,12 @@ private async Task x.TreeId == treeId); - var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.Id}"; - return (categoryPath, null)!; + var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.MachineName}"; + return (categoryPath, parent!.Id, null); } #region EntityConfiguration @@ -1305,7 +1305,7 @@ await GetAttributeConfigurationsForEntityConfiguration( ).ConfigureAwait(false); - (var categoryPath, ProblemDetails? errors) = + (var categoryPath, Guid? parentId, ProblemDetails? errors) = await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); if (errors != null) @@ -1319,6 +1319,7 @@ await GetAttributeConfigurationsForEntityConfiguration( _mapper.Map>(categoryCreateRequest.Attributes), categoryCreateRequest.TenantId, categoryPath!, + parentId!.Value, categoryCreateRequest.CategoryTreeId ); @@ -2438,7 +2439,7 @@ public async Task> QueryInstancesJsonSingleL return (null, new ValidationErrorResponse(nameof(entityInstanceId), "Instance not found"))!; } - (var newCategoryPath, ProblemDetails? errors) = + (var newCategoryPath, var parentId, ProblemDetails? errors) = await BuildCategoryPath(treeId, newParentId, cancellationToken).ConfigureAwait(false); if (errors != null) @@ -2446,7 +2447,7 @@ public async Task> QueryInstancesJsonSingleL return (null, errors)!; } - entityInstance.ChangeCategoryPath(treeId, newCategoryPath ?? ""); + entityInstance.ChangeCategoryPath(treeId, newCategoryPath ?? "", parentId!.Value); var saved = await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken) .ConfigureAwait(false); if (!saved) diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index 32a4fe5..a1c2b7c 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -1,4 +1,4 @@ -using CloudFabric.EAV.Models.RequestModels; + using CloudFabric.EAV.Models.RequestModels; using CloudFabric.EAV.Models.ViewModels; using CloudFabric.EAV.Tests.Factories; using CloudFabric.EventSourcing.EventStore; From e9cd82e343a184b80e328950d09b1768d04ecfb8 Mon Sep 17 00:00:00 2001 From: Dmytro Honcharenko Date: Mon, 26 Jun 2023 12:59:27 +0300 Subject: [PATCH 2/9] Refactored queries --- .../Entity/EntityCategoryPathChanged.cs | 6 +- CloudFabric.EAV.Domain/Models/Category.cs | 14 +- CloudFabric.EAV.Domain/Models/CategoryPath.cs | 2 +- .../Models/EntityInstanceBase.cs | 6 +- .../EntityInstanceProjectionBuilder.cs | 32 +- .../ProjectionAttributesSchemaFactory.cs | 16 + .../ViewModels/CategoryPathViewModel.cs | 2 + .../ViewModels/CategoryViewModel.cs | 4 +- CloudFabric.EAV.Service/EAVCategoryService.cs | 739 ++++++++++++++++++ CloudFabric.EAV.Service/EAVService.cs | 132 ++-- ...CreateUpdateRequestFromJsonDeserializer.cs | 4 +- .../CategoryTests/CategoryTests.cs | 73 +- .../Factories/EntityInstanceFactory.cs | 4 +- .../JsonSerializationTests.cs | 1 + 14 files changed, 932 insertions(+), 103 deletions(-) create mode 100644 CloudFabric.EAV.Service/EAVCategoryService.cs diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs index 85a38ea..11cfe3d 100644 --- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs @@ -14,20 +14,20 @@ public EntityCategoryPathChanged(Guid id, Guid entityConfigurationId, Guid categoryTreeId, string categoryPath, - Guid parentId) + Guid? parentId) { AggregateId = id; EntityConfigurationId = entityConfigurationId; CategoryPath = categoryPath; CategoryTreeId = categoryTreeId; ParentId = parentId; - ParentMachineName = categoryPath.Split('/').Last(x => !string.IsNullOrEmpty(x)); + ParentMachineName = string.IsNullOrEmpty(categoryPath) ? "" : categoryPath.Split('/').Last(x => !string.IsNullOrEmpty(x)); } public string CategoryPath { get; set; } public Guid EntityConfigurationId { get; set; } public Guid CategoryTreeId { get; set; } - public Guid ParentId { get; set; } + public Guid? ParentId { get; set; } public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/Category.cs b/CloudFabric.EAV.Domain/Models/Category.cs index a3f6f87..b7fb109 100644 --- a/CloudFabric.EAV.Domain/Models/Category.cs +++ b/CloudFabric.EAV.Domain/Models/Category.cs @@ -15,9 +15,9 @@ public Category(Guid id, Guid entityConfigurationId, List attributes, Guid? tenantId) - : base(id, entityConfigurationId, attributes, tenantId) { Apply(new CategoryCreated(id, machineName, entityConfigurationId, attributes, tenantId)); + } public Category( @@ -27,10 +27,20 @@ public Category( List attributes, Guid? tenantId, string categoryPath, - Guid parentId, + Guid? parentId, Guid categoryTreeId ) : this(id, machineName, entityConfigurationId, attributes, tenantId) { Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath, parentId)); } + + public void On(CategoryCreated @event) + { + Id = @event.AggregateId; + EntityConfigurationId = @event.EntityConfigurationId; + Attributes = new List(@event.Attributes).AsReadOnly(); + TenantId = @event.TenantId; + CategoryPaths = new List(); + MachineName = @event.MachineName; + } } diff --git a/CloudFabric.EAV.Domain/Models/CategoryPath.cs b/CloudFabric.EAV.Domain/Models/CategoryPath.cs index 30d3d3b..1b2ac03 100644 --- a/CloudFabric.EAV.Domain/Models/CategoryPath.cs +++ b/CloudFabric.EAV.Domain/Models/CategoryPath.cs @@ -4,6 +4,6 @@ public class CategoryPath { public Guid TreeId { get; set; } public string Path { get; set; } - public Guid ParentId { get; set; } + public Guid? ParentId { get; set; } public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs index 250f803..3ffa85a 100644 --- a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs +++ b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs @@ -9,7 +9,7 @@ namespace CloudFabric.EAV.Domain.Models; public class EntityInstanceBase : AggregateBase { - public EntityInstanceBase(IEnumerable events) : base(events) + public EntityInstanceBase(IEnumerable events) : base(events) { } @@ -19,6 +19,10 @@ public EntityInstanceBase(Guid id, Guid entityConfigurationId, List EntityConfigurationId.ToString(); public List CategoryPaths { get; protected set; } diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs index 27ec493..e0804e9 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs @@ -31,7 +31,8 @@ namespace CloudFabric.EAV.Domain.Projections.EntityInstanceProjection; /// public class EntityInstanceProjectionBuilder : ProjectionBuilder, IHandleEvent, - // IHandleEvent, + IHandleEvent, +// IHandleEvent, IHandleEvent, // IHandleEvent, IHandleEvent, @@ -183,6 +184,35 @@ await UpsertDocument( ); } + public async Task On(CategoryCreated @event) + { + ProjectionDocumentSchema projectionDocumentSchema = + await BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( + @event.EntityConfigurationId + ).ConfigureAwait(false); + + var document = new Dictionary + { + { "Id", @event.AggregateId }, + { "EntityConfigurationId", @event.EntityConfigurationId }, + { "TenantId", @event.TenantId }, + { "CategoryPaths", new List() }, + { "MachineName", @event.MachineName}, + }; + + foreach (AttributeInstance attribute in @event.Attributes) + { + document.Add(attribute.ConfigurationAttributeMachineName, attribute.GetValue()); + } + + await UpsertDocument( + projectionDocumentSchema, + document, + @event.PartitionKey, + @event.Timestamp + ); + } + private async Task BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( Guid entityConfigurationId ) diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs index 3ce1e85..8950122 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs @@ -441,6 +441,22 @@ private static List GetCategoryPathsNestedProp IsRetrievable = true, IsFilterable = true, IsSortable = true + }, + new () + { + PropertyName = "ParentMachineName", + PropertyType = TypeCode.String, + IsRetrievable = true, + IsFilterable = true, + IsSortable = true + }, + new () + { + PropertyName = "ParentId", + PropertyType = TypeCode.Object, + IsRetrievable = true, + IsFilterable = true, + IsSortable = true } }; } diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs index 68375bf..22708b3 100644 --- a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs @@ -4,4 +4,6 @@ public class CategoryPathViewModel { public Guid TreeId { get; set; } public string Path { get; set; } + public Guid? ParentId { get; set; } + public string ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs index a1aa458..b224d60 100644 --- a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs @@ -3,8 +3,8 @@ namespace CloudFabric.EAV.Models.ViewModels; // As the domain model EntityInstanceBase presents a vast array of features // and represents both EntityInstance and Category with the same properties set, // it is preferable for one of the models to be inherited from another, -// in order to avoid code overload and repeats. +// in order to avoid code overload and repeats. public class CategoryViewModel : EntityInstanceViewModel { - + public string MachineName { get; set; } } diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs new file mode 100644 index 0000000..ac7ecc7 --- /dev/null +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -0,0 +1,739 @@ +// using System.Diagnostics.CodeAnalysis; +// using System.Text.Json; +// +// using AutoMapper; +// +// using CloudFabric.EAV.Domain.Models; +// using CloudFabric.EAV.Models.RequestModels; +// using CloudFabric.EAV.Models.ViewModels; +// using CloudFabric.EventSourcing.Domain; +// using CloudFabric.EventSourcing.EventStore; +// using CloudFabric.EventSourcing.EventStore.Persistence; +// using CloudFabric.Projections; +// using CloudFabric.Projections.Queries; +// +// using Microsoft.AspNetCore.Mvc; +// +// using ProjectionDocumentSchemaFactory = CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; +// +// namespace CloudFabric.EAV.Service; +// +// public class EAVCategoryService +// { +// private readonly AggregateRepository _entityConfigurationRepository; +// private readonly AggregateRepository _categoryTreeRepository; +// private readonly AggregateRepository _categoryInstanceRepository; +// private readonly IMapper _mapper; +// private readonly EventUserInfo _userInfo; +// private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; +// +// +// public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( +// CategoryTreeCreateRequest entity, +// Guid? tenantId, +// CancellationToken cancellationToken = default +// ) +// { +// EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( +// entity.EntityConfigurationId, +// entity.EntityConfigurationId.ToString(), +// cancellationToken +// ).ConfigureAwait(false); +// +// if (entityConfiguration == null) +// { +// return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; +// } +// +// var tree = new CategoryTree( +// Guid.NewGuid(), +// entity.EntityConfigurationId, +// entity.MachineName, +// tenantId +// ); +// +// _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); +// return (_mapper.Map(tree), null)!; +// } +// +// /// +// /// Create new category from provided json string. +// /// +// /// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description", +// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", +// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", +// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", +// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names, +// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, +// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories +// /// "parentId" - id guid of category from which new branch of hierarchy will be built. +// /// Can be null if placed at the root of category tree. +// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant +// /// application this should be one hardcoded guid for whole app. +// /// +// /// +// /// +// /// +// /// (CategoryInstanceCreateRequest createRequest); ]]> +// /// +// /// This function will be called after deserializing the request from json +// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. +// /// +// /// +// /// +// public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( +// string categoryJsonString, +// Func>? requestDeserializedCallback = null, +// CancellationToken cancellationToken = default +// ) +// { +// JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); +// +// return CreateCategoryInstance( +// categoryJson.RootElement, +// requestDeserializedCallback, +// cancellationToken +// ); +// } +// +// /// +// /// Create new category from provided json string. +// /// +// /// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names. +// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, +// /// so they should not be in json. +// /// +// /// +// /// +// /// id of entity configuration which has all category attributes +// /// id of category tree, which represents separated hirerarchy with relations between categories +// /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. +// /// tenant id guid. A guid which uniquely identifies and isolates the data. For single +// /// tenant application this should be one hardcoded guid for whole app. +// /// +// /// (CategoryInstanceCreateRequest createRequest); ]]> +// /// +// /// This function will be called after deserializing the request from json +// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. +// /// +// /// +// /// +// public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( +// string categoryJsonString, +// Guid categoryConfigurationId, +// Guid categoryTreeId, +// Guid? parentId, +// Guid? tenantId, +// Func>? requestDeserializedCallback = null, +// CancellationToken cancellationToken = default +// ) +// { +// JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); +// +// return CreateCategoryInstance( +// categoryJson.RootElement, +// categoryConfigurationId, +// categoryTreeId, +// parentId, +// tenantId, +// requestDeserializedCallback, +// cancellationToken +// ); +// } +// +// /// +// /// Create new category from provided json document. +// /// +// /// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description", +// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", +// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", +// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", +// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names, +// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, +// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories +// /// "parentId" - id guid of category from which new branch of hierarchy will be built. +// /// Can be null if placed at the root of category tree. +// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant +// /// application this should be one hardcoded guid for whole app. +// /// +// /// +// /// +// /// +// /// (CategoryInstanceCreateRequest createRequest); ]]> +// /// +// /// This function will be called after deserializing the request from json +// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. +// /// +// /// +// /// +// public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( +// JsonElement categoryJson, +// Func>? requestDeserializedCallback = null, +// CancellationToken cancellationToken = default +// ) +// { +// var (categoryInstanceCreateRequest, deserializationErrors) = +// await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken); +// +// if (deserializationErrors != null) +// { +// return (null, deserializationErrors); +// } +// +// return await CreateCategoryInstance( +// categoryJson, +// categoryInstanceCreateRequest!.CategoryConfigurationId, +// categoryInstanceCreateRequest.CategoryTreeId, +// categoryInstanceCreateRequest.ParentId, +// categoryInstanceCreateRequest.TenantId, +// requestDeserializedCallback, +// cancellationToken +// ); +// } +// +// /// +// /// Create new category from provided json document. +// /// +// /// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names. +// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, +// /// so they should not be in json. +// /// +// /// +// /// +// /// id of entity configuration which has all category attributes +// /// id of category tree, which represents separated hirerarchy with relations between categories +// /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. +// /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single +// /// tenant application this should be one hardcoded guid for whole app. +// /// +// /// (CategoryInstanceCreateRequest createRequest); ]]> +// /// +// /// This function will be called after deserializing the request from json +// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. +// /// +// /// +// /// +// public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( +// JsonElement categoryJson, +// Guid categoryConfigurationId, +// Guid categoryTreeId, +// Guid? parentId, +// Guid? tenantId, +// Func>? requestDeserializedCallback = null, +// CancellationToken cancellationToken = default +// ) +// { +// (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) +// = await DeserializeCategoryInstanceCreateRequestFromJson( +// categoryJson, +// categoryConfigurationId, +// categoryTreeId, +// parentId, +// tenantId, +// cancellationToken +// ); +// +// if (deserializationErrors != null) +// { +// return (null, deserializationErrors); +// } +// +// if (requestDeserializedCallback != null) +// { +// categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); +// } +// +// var (createdCategory, validationErrors) = await CreateCategoryInstance( +// categoryInstanceCreateRequest!, cancellationToken +// ); +// +// if (validationErrors != null) +// { +// return (null, validationErrors); +// } +// +// return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); +// } +// +// public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( +// CategoryInstanceCreateRequest categoryCreateRequest, +// CancellationToken cancellationToken = default +// ) +// { +// CategoryTree? tree = await _categoryTreeRepository.LoadAsync( +// categoryCreateRequest.CategoryTreeId, +// categoryCreateRequest.CategoryTreeId.ToString(), +// cancellationToken +// ).ConfigureAwait(false); +// +// if (tree == null) +// { +// return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; +// } +// +// if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) +// { +// return (null, +// new ValidationErrorResponse("CategoryConfigurationId", +// "Category tree uses another configuration for categories" +// ))!; +// } +// +// EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( +// categoryCreateRequest.CategoryConfigurationId, +// categoryCreateRequest.CategoryConfigurationId.ToString(), +// cancellationToken +// ).ConfigureAwait(false); +// +// +// if (entityConfiguration == null) +// { +// return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; +// } +// +// List attributeConfigurations = +// await GetAttributeConfigurationsForEntityConfiguration( +// entityConfiguration, +// cancellationToken +// ).ConfigureAwait(false); +// +// +// (var categoryPath, Guid? parentId, ProblemDetails? errors) = +// await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); +// +// if (errors != null) +// { +// return (null, errors)!; +// } +// +// var categoryInstance = new Category( +// Guid.NewGuid(), +// categoryCreateRequest.MachineName, +// categoryCreateRequest.CategoryConfigurationId, +// _mapper.Map>(categoryCreateRequest.Attributes), +// categoryCreateRequest.TenantId, +// categoryPath!, +// parentId, +// categoryCreateRequest.CategoryTreeId +// ); +// +// var validationErrors = new Dictionary(); +// foreach (AttributeConfiguration a in attributeConfigurations) +// { +// AttributeInstance? attributeValue = categoryInstance.Attributes +// .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); +// +// List attrValidationErrors = a.ValidateInstance(attributeValue); +// if (attrValidationErrors is { Count: > 0 }) +// { +// validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); +// } +// } +// +// if (validationErrors.Count > 0) +// { +// return (null, new ValidationErrorResponse(validationErrors))!; +// } +// +// ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory +// .FromEntityConfiguration(entityConfiguration, attributeConfigurations); +// +// IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); +// await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); +// +// var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) +// .ConfigureAwait(false); +// if (!saved) +// { +// //TODO: What do we want to do with internal exceptions and unsuccessful flow? +// throw new Exception("Entity was not saved"); +// } +// +// return (_mapper.Map(categoryInstance), null)!; +// } +// +// /// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description", +// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", +// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", +// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", +// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names, +// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, +// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories +// /// "parentId" - id guid of category from which new branch of hierarchy will be built. +// /// Can be null if placed at the root of category tree. +// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant +// /// application this should be one hardcoded guid for whole app. +// /// +// /// +// public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( +// JsonElement categoryJson, +// CancellationToken cancellationToken = default +// ) +// { +// Guid categoryConfigurationId; +// if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) +// { +// if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) +// { +// categoryConfigurationId = categoryConfigurationIdGuid; +// } +// else +// { +// return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; +// } +// } +// else +// { +// return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); +// } +// +// Guid categoryTreeId; +// if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) +// { +// if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) +// { +// categoryTreeId = categoryTreeIdGuid; +// } +// else +// { +// return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; +// } +// } +// else +// { +// return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); +// } +// +// Guid? parentId = null; +// if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) +// { +// if (parentIdJsonElement.ValueKind == JsonValueKind.Null) +// { +// parentId = null; +// } +// else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) +// { +// parentId = parentIdGuid; +// } +// else +// { +// return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; +// } +// } +// +// Guid? tenantId = null; +// if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) +// { +// if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) +// { +// tenantId = null; +// } +// else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) +// { +// tenantId = tenantIdGuid; +// } +// else +// { +// return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; +// } +// } +// +// return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); +// } +// +// /// Use following json format: +// /// +// /// ``` +// /// { +// /// "name": "Main Category", +// /// "desprition": "Main Category description" +// /// } +// /// ``` +// /// +// /// Where "name" and "description" are attributes machine names. +// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, +// /// so they should not be in json. +// /// +// /// +// public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( +// JsonElement categoryJson, +// Guid categoryConfigurationId, +// Guid categoryTreeId, +// Guid? parentId, +// Guid? tenantId, +// CancellationToken cancellationToken = default +// ) +// { +// EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( +// categoryConfigurationId, +// categoryConfigurationId.ToString(), +// cancellationToken +// ) +// .ConfigureAwait(false); +// +// if (categoryConfiguration == null) +// { +// return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; +// } +// +// List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( +// categoryConfiguration, +// cancellationToken +// ) +// .ConfigureAwait(false); +// +// return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( +// categoryConfigurationId, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson +// ); +// } +// +// /// +// /// Returns full category tree. +// /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. +// /// +// /// +// /// +// /// +// +// [SuppressMessage("Performance", "CA1806:Do not ignore method results")] +// public async Task> GetCategoryTreeViewAsync( +// Guid treeId, +// Guid? notDeeperThanCategoryId = null, +// CancellationToken cancellationToken = default +// ) +// { +// CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) +// .ConfigureAwait(false); +// if (tree == null) +// { +// throw new NotFoundException("Category tree not found"); +// } +// +// ProjectionQueryResult treeElementsQueryResult = +// await QueryInstances(tree.EntityConfigurationId, +// new ProjectionQuery +// { +// Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, +// Limit = _elasticSearchQueryOptions.MaxSize +// }, +// cancellationToken +// ).ConfigureAwait(false); +// +// var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); +// +// int searchedLevelPathLenght; +// +// if (notDeeperThanCategoryId != null) +// { +// var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); +// +// if (category == null) +// { +// throw new NotFoundException("Category not found"); +// } +// +// searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; +// +// treeElements = treeElements +// .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); +// } +// +// var treeViewModel = new List(); +// +// // Go through each instance once +// foreach (EntityInstanceViewModel treeElement in treeElements +// .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) +// { +// var treeElementViewModel = _mapper.Map(treeElement); +// var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; +// +// if (string.IsNullOrEmpty(categoryPath)) +// { +// treeViewModel.Add(treeElementViewModel); +// } +// else +// { +// IEnumerable categoryPathElements = +// categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); +// EntityTreeInstanceViewModel? currentLevel = null; +// categoryPathElements.Aggregate(treeViewModel, +// (acc, pathComponent) => +// { +// EntityTreeInstanceViewModel? parent = +// acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); +// if (parent == null) +// { +// EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); +// parent = _mapper.Map(parentInstance); +// acc.Add(parent); +// } +// +// currentLevel = parent; +// return parent.Children; +// } +// ); +// currentLevel?.Children.Add(treeElementViewModel); +// } +// } +// +// return treeViewModel; +// } +// +// /// +// /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. +// /// +// /// +// /// +// /// +// public async Task> GetSubcategories( +// Guid categoryTreeId, +// Guid? parentId, +// CancellationToken cancellationToken = default +// ) +// { +// var categoryTree = await _categoryTreeRepository.LoadAsync( +// categoryTreeId, categoryTreeId.ToString(), cancellationToken +// ).ConfigureAwait(false); +// +// if (categoryTree == null) +// { +// throw new NotFoundException("Category tree not found"); +// } +// +// var query = await GetSubcategoriesPrepareQuery(categoryTreeId, parentId, cancellationToken); +// +// var queryResult = _mapper.Map>( +// await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) +// ); +// +// return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); +// } +// +// private async Task GetSubcategoriesPrepareQuery( +// Guid categoryTreeId, +// Guid? parentId, +// CancellationToken cancellationToken = default +// ) +// { +// var categoryTree = await _categoryTreeRepository.LoadAsync( +// categoryTreeId, categoryTreeId.ToString(), cancellationToken +// ).ConfigureAwait(false); +// +// if (categoryTree == null) +// { +// throw new NotFoundException("Category tree not found"); +// } +// +// ProjectionQuery query = new ProjectionQuery +// { +// Limit = _elasticSearchQueryOptions.MaxSize +// }; +// +// if (parentId == null) +// { +// query.Filters.AddRange( +// +// new List +// { +// new Filter +// { +// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", +// Operator = FilterOperator.Equal, +// Value = categoryTree.Id.ToString(), +// }, +// new Filter +// { +// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", +// Operator = FilterOperator.Equal, +// Value = string.Empty +// } +// } +// ); +// +// return query; +// } +// +// var category = await _categoryInstanceRepository.LoadAsync( +// parentId.Value, categoryTree.EntityConfigurationId.ToString(), cancellationToken +// ).ConfigureAwait(false); +// +// if (category == null) +// { +// throw new NotFoundException("Category not found"); +// } +// +// string categoryPath = category.CategoryPaths.Where(x => x.TreeId == categoryTree.Id) +// .Select(p => p.Path).FirstOrDefault()!; +// +// query = new ProjectionQuery +// { +// Filters = new List +// { +// new Filter +// { +// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", +// Operator = FilterOperator.Equal, +// Value = categoryTree.Id.ToString(), +// }, +// new Filter +// { +// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", +// Operator = FilterOperator.Equal, +// Value = categoryPath + $"/{category.Id}" +// } +// } +// }; +// +// return query; +// } +// } diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 8f96e0c..4b55f01 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -52,6 +52,7 @@ private readonly IProjectionRepository private readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer _entityInstanceCreateUpdateRequestFromJsonDeserializer; private readonly AggregateRepository _entityInstanceRepository; + private readonly AggregateRepository _categoryInstanceRepository; private readonly ILogger _logger; private readonly IMapper _mapper; private readonly JsonSerializerOptions _jsonSerializerOptions; @@ -90,6 +91,8 @@ public EAVService( .GetAggregateRepository(); _entityInstanceRepository = _aggregateRepositoryFactory .GetAggregateRepository(); + _categoryInstanceRepository = _aggregateRepositoryFactory + .GetAggregateRepository(); _categoryTreeRepository = _aggregateRepositoryFactory .GetAggregateRepository(); @@ -283,7 +286,7 @@ private async Task(await _entityInstanceRepository + : _mapper.Map(await _categoryInstanceRepository .LoadAsync(parentId.Value, tree.EntityConfigurationId.ToString(), cancellationToken) .ConfigureAwait(false) ); @@ -295,7 +298,7 @@ private async Task x.TreeId == treeId); var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.MachineName}"; - return (categoryPath, parent!.Id, null); + return (categoryPath, parent?.Id, null); } #region EntityConfiguration @@ -1092,6 +1095,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR /// /// /// + /// /// id of entity configuration which has all category attributes /// id of category tree, which represents separated hirerarchy with relations between categories /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. @@ -1107,6 +1111,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR /// public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( string categoryJsonString, + string machineName, Guid categoryConfigurationId, Guid categoryTreeId, Guid? parentId, @@ -1119,6 +1124,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR return CreateCategoryInstance( categoryJson.RootElement, + machineName, categoryConfigurationId, categoryTreeId, parentId, @@ -1170,7 +1176,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR ) { var (categoryInstanceCreateRequest, deserializationErrors) = - await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken); + await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken); if (deserializationErrors != null) { @@ -1179,6 +1185,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR return await CreateCategoryInstance( categoryJson, + categoryInstanceCreateRequest!.MachineName, categoryInstanceCreateRequest!.CategoryConfigurationId, categoryInstanceCreateRequest.CategoryTreeId, categoryInstanceCreateRequest.ParentId, @@ -1222,6 +1229,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR /// public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( JsonElement categoryJson, + string machineName, Guid categoryConfigurationId, Guid categoryTreeId, Guid? parentId, @@ -1233,6 +1241,7 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) = await DeserializeCategoryInstanceCreateRequestFromJson( categoryJson, + machineName, categoryConfigurationId, categoryTreeId, parentId, @@ -1315,11 +1324,12 @@ await GetAttributeConfigurationsForEntityConfiguration( var categoryInstance = new Category( Guid.NewGuid(), + categoryCreateRequest.MachineName, categoryCreateRequest.CategoryConfigurationId, _mapper.Map>(categoryCreateRequest.Attributes), categoryCreateRequest.TenantId, categoryPath!, - parentId!.Value, + parentId, categoryCreateRequest.CategoryTreeId ); @@ -1341,15 +1351,13 @@ await GetAttributeConfigurationsForEntityConfiguration( return (null, new ValidationErrorResponse(validationErrors))!; } - var mappedInstance = _mapper.Map(categoryInstance); - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory .FromEntityConfiguration(entityConfiguration, attributeConfigurations); IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - var saved = await _entityInstanceRepository.SaveAsync(_userInfo, mappedInstance, cancellationToken) + var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) .ConfigureAwait(false); if (!saved) { @@ -1456,7 +1464,17 @@ await GetAttributeConfigurationsForEntityConfiguration( } } - return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); + string? machineName = null; + if (categoryJson.TryGetProperty("machineName", out var machineNameJsonElement)) + { + machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString(); + if (machineName == null) + { + return (null, new ValidationErrorResponse("machineName", "Value is not a valid")); + } + } + + return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); } /// Use following json format: @@ -1475,6 +1493,7 @@ await GetAttributeConfigurationsForEntityConfiguration( /// public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( JsonElement categoryJson, + string machineName, Guid categoryConfigurationId, Guid categoryTreeId, Guid? parentId, @@ -1501,7 +1520,7 @@ await GetAttributeConfigurationsForEntityConfiguration( .ConfigureAwait(false); return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( - categoryConfigurationId, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson + categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson ); } @@ -1605,7 +1624,8 @@ await QueryInstances(tree.EntityConfigurationId, /// public async Task> GetSubcategories( Guid categoryTreeId, - Guid? parentId, + Guid? parentId = null, + string? parentMachineName = null, CancellationToken cancellationToken = default ) { @@ -1618,7 +1638,7 @@ await QueryInstances(tree.EntityConfigurationId, throw new NotFoundException("Category tree not found"); } - var query = await GetSubcategoriesPrepareQuery(categoryTreeId, parentId, cancellationToken); + var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken); var queryResult = _mapper.Map>( await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) @@ -1627,82 +1647,60 @@ await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToke return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); } - private async Task GetSubcategoriesPrepareQuery( - Guid categoryTreeId, + private ProjectionQuery GetSubcategoriesPrepareQuery( + CategoryTree categoryTree, Guid? parentId, + string? parentMachineName, CancellationToken cancellationToken = default ) { - var categoryTree = await _categoryTreeRepository.LoadAsync( - categoryTreeId, categoryTreeId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (categoryTree == null) - { - throw new NotFoundException("Category tree not found"); - } - ProjectionQuery query = new ProjectionQuery { Limit = _elasticSearchQueryOptions.MaxSize }; - if (parentId == null) + query.Filters.Add(new Filter { - query.Filters.AddRange( - - new List - { - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", - Operator = FilterOperator.Equal, - Value = categoryTree.Id.ToString(), - }, - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", - Operator = FilterOperator.Equal, - Value = string.Empty - } - } - ); + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", + Operator = FilterOperator.Equal, + Value = categoryTree.Id.ToString(), + }); + // If nothing is set - get subcategories of master level + if (parentId == null && string.IsNullOrEmpty(parentMachineName)) + { + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = string.Empty, + }); return query; } - var category = await _entityInstanceRepository.LoadAsync( - parentId.Value, categoryTree.EntityConfigurationId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (category == null) + if (parentId != null) { - throw new NotFoundException("Category not found"); - } - string categoryPath = category.CategoryPaths.Where(x => x.TreeId == categoryTree.Id) - .Select(p => p.Path).FirstOrDefault()!; + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", + Operator = FilterOperator.Equal, + Value = parentId.ToString() + }); + } - query = new ProjectionQuery + if (!string.IsNullOrEmpty(parentMachineName)) { - Filters = new List - { - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", - Operator = FilterOperator.Equal, - Value = categoryTree.Id.ToString(), - }, - new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", - Operator = FilterOperator.Equal, - Value = categoryPath + $"/{category.Id}" - } - } - }; + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = parentMachineName + }); + } return query; + } #endregion diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs index 4b7e067..47a4840 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceCreateUpdateRequestFromJsonDeserializer.cs @@ -52,6 +52,7 @@ JsonElement record public async Task<(CategoryInstanceCreateRequest?, ValidationErrorResponse?)> DeserializeCategoryInstanceCreateRequest( Guid categoryConfigurationId, + string machineName, Guid? tenantId, Guid categoryTreeId, Guid? parentId, @@ -73,7 +74,8 @@ JsonElement record CategoryTreeId = categoryTreeId, ParentId = parentId, TenantId = tenantId, - Attributes = attributes + Attributes = attributes, + MachineName = machineName }; return (categoryInstanceCreateRequest, null); diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index a1c2b7c..64431ee 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -16,6 +16,12 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public abstract class CategoryTests : BaseQueryTests.BaseQueryTests { + private const string _laptopsCategoryMachineName = "laptops"; + private const string _gamingLaptopsCategoryMachineName = "gaming-laptops"; + private const string _officeLaptopsCategoryMachineName = "office-laptops"; + private const string _asusGamingLaptopsCategoryMachineName = "asus-gaming-laptops"; + private const string _rogAsusGamingLaptopsCategoryMachineName = "rog-gaming-laptops"; + private async Task<(HierarchyViewModel tree, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, @@ -43,35 +49,55 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests CancellationToken.None ); - CategoryInstanceCreateRequest categoryInstanceRequest = - EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + (CategoryViewModel laptopsCategory, _) = + await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, null, categoryConfigurationCreateRequest.TenantId, + _laptopsCategoryMachineName, 0, 9 - ); - + )); - (CategoryViewModel laptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); - - categoryInstanceRequest.ParentId = laptopsCategory.Id; (CategoryViewModel gamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + laptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _gamingLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = laptopsCategory.Id; (CategoryViewModel officeLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + laptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _officeLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = gamingLaptopsCategory.Id; (CategoryViewModel asusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); + await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + gamingLaptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _asusGamingLaptopsCategoryMachineName, + 0, + 9 + )); - categoryInstanceRequest.ParentId = asusGamingLaptopsCategory.Id; (CategoryViewModel rogAsusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(categoryInstanceRequest); - + await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + createdTree.Id, + asusGamingLaptopsCategory.Id, + categoryConfigurationCreateRequest.TenantId, + _rogAsusGamingLaptopsCategoryMachineName, + 0, + 9 + )); await Task.Delay(ProjectionsUpdateDelay); return (createdTree, laptopsCategory, gamingLaptopsCategory, officeLaptopsCategory, @@ -87,7 +113,7 @@ public async Task CreateCategory_Success() laptopsCategory.Id.Should().NotBeEmpty(); laptopsCategory.Attributes.Count.Should().Be(9); laptopsCategory.TenantId.Should().NotBeNull(); - gamingLaptopsCategory.CategoryPaths.Should().Contain(x => x.Path.Contains(laptopsCategory.Id.ToString())); + gamingLaptopsCategory.CategoryPaths.Should().Contain(x => x.Path.Contains(laptopsCategory.MachineName)); } [TestMethod] @@ -151,7 +177,7 @@ public async Task GetSubcategoriesBranch_Success() _, _, _) = await BuildTestTreeAsync(); await Task.Delay(ProjectionsUpdateDelay); - var categoryPathValue = $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}"; + var categoryPathValue = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}"; ProjectionQueryResult subcategories12 = await _eavService.QueryInstances( createdTree.EntityConfigurationId, new ProjectionQuery @@ -197,7 +223,7 @@ public async Task GetSubcategories_TreeNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetSubcategories(Guid.NewGuid(), null); + Func action = async () => await _eavService.GetSubcategories(Guid.NewGuid()); await action.Should().ThrowAsync().WithMessage("Category tree not found"); } @@ -209,9 +235,8 @@ public async Task GetSubcategories_ParentNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetSubcategories(createdTree.Id, Guid.NewGuid()); - - await action.Should().ThrowAsync().WithMessage("Category not found"); + var result = await _eavService.GetSubcategories(createdTree.Id, parentId: Guid.NewGuid()); + result.Should().BeEmpty(); } [TestMethod] @@ -253,7 +278,7 @@ public async Task MoveAndGetItemsFromCategory_Success() await Task.Delay(ProjectionsUpdateDelay); - var pathFilterValue121 = $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}/{asusGamingLaptops.Id}"; + var pathFilterValue121 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}"; ProjectionQueryResult itemsFrom121 = await _eavService.QueryInstances( @@ -268,7 +293,7 @@ public async Task MoveAndGetItemsFromCategory_Success() } ); var pathFilterValue1211 = - $"/{laptopsCategory.Id}/{gamingLaptopsCategory.Id}/{asusGamingLaptops.Id}/{rogAsusGamingLaptops.Id}"; + $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}/{_rogAsusGamingLaptopsCategoryMachineName}"; ProjectionQueryResult itemsFrom1211 = await _eavService.QueryInstances( itemEntityConfiguration.Id, diff --git a/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs b/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs index a342f71..0fcd9b6 100644 --- a/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs +++ b/CloudFabric.EAV.Tests/Factories/EntityInstanceFactory.cs @@ -11,6 +11,7 @@ public static CategoryInstanceCreateRequest CreateCategoryInstanceRequest(Guid e Guid treeId, Guid? parentId, Guid? tenantId, + string machineName, int attributeIndexFrom = 0, int attributeIndexTo = 1) { @@ -30,7 +31,8 @@ public static CategoryInstanceCreateRequest CreateCategoryInstanceRequest(Guid e Attributes = attributeInstances, ParentId = parentId, TenantId = tenantId, - CategoryTreeId = treeId + CategoryTreeId = treeId, + MachineName = machineName }; } diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index fa43e18..386d83a 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -310,6 +310,7 @@ public async Task CreateCategoryInstance() (createdCategory, _) = await _eavService.CreateCategoryInstance( categoryJsonStringCreateRequest, + "test-category", createdCategoryConfiguration.Id, hierarchy.Id, null, From c976a09c3e5d2dcb49d7ecd78f6ec32e3ebae838 Mon Sep 17 00:00:00 2001 From: Dmytro Honcharenko Date: Tue, 27 Jun 2023 16:51:48 +0300 Subject: [PATCH 3/9] {{WIP}} Splitted services --- .../EntityInstanceUpdateRequest.cs | 5 + .../ViewModels/EntityInstanceViewModel.cs | 2 + CloudFabric.EAV.Service/EAVCategoryService.cs | 1500 +++++++++-------- .../EAVEntityInstanceService.cs | 533 ++++++ CloudFabric.EAV.Service/EAVService.cs | 1284 +------------- CloudFabric.EAV.Service/IEAVService.cs | 5 - ...ntityInstanceFromDictionaryDeserializer.cs | 155 +- 7 files changed, 1444 insertions(+), 2040 deletions(-) create mode 100644 CloudFabric.EAV.Service/EAVEntityInstanceService.cs delete mode 100644 CloudFabric.EAV.Service/IEAVService.cs diff --git a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs index 00d6477..1eec2ea 100755 --- a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs @@ -9,3 +9,8 @@ public class EntityInstanceUpdateRequest public List AttributesToAddOrUpdate { get; set; } public List? AttributeMachineNamesToRemove { get; set; } } + +public class CategoryUpdateRequest: EntityInstanceUpdateRequest +{ + +} diff --git a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs index 4f8e156..da8f9f4 100755 --- a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs @@ -21,6 +21,8 @@ public class EntityTreeInstanceViewModel { public Guid Id { get; set; } + public string MachineName { get; set; } + public Guid EntityConfigurationId { get; set; } public List Attributes { get; set; } diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs index ac7ecc7..b4d8e84 100644 --- a/CloudFabric.EAV.Service/EAVCategoryService.cs +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -1,739 +1,761 @@ -// using System.Diagnostics.CodeAnalysis; -// using System.Text.Json; -// -// using AutoMapper; -// -// using CloudFabric.EAV.Domain.Models; -// using CloudFabric.EAV.Models.RequestModels; -// using CloudFabric.EAV.Models.ViewModels; -// using CloudFabric.EventSourcing.Domain; -// using CloudFabric.EventSourcing.EventStore; -// using CloudFabric.EventSourcing.EventStore.Persistence; -// using CloudFabric.Projections; -// using CloudFabric.Projections.Queries; -// -// using Microsoft.AspNetCore.Mvc; -// -// using ProjectionDocumentSchemaFactory = CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; -// -// namespace CloudFabric.EAV.Service; -// -// public class EAVCategoryService -// { -// private readonly AggregateRepository _entityConfigurationRepository; -// private readonly AggregateRepository _categoryTreeRepository; -// private readonly AggregateRepository _categoryInstanceRepository; -// private readonly IMapper _mapper; -// private readonly EventUserInfo _userInfo; -// private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; -// -// -// public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( -// CategoryTreeCreateRequest entity, -// Guid? tenantId, -// CancellationToken cancellationToken = default -// ) -// { -// EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( -// entity.EntityConfigurationId, -// entity.EntityConfigurationId.ToString(), -// cancellationToken -// ).ConfigureAwait(false); -// -// if (entityConfiguration == null) -// { -// return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; -// } -// -// var tree = new CategoryTree( -// Guid.NewGuid(), -// entity.EntityConfigurationId, -// entity.MachineName, -// tenantId -// ); -// -// _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); -// return (_mapper.Map(tree), null)!; -// } -// -// /// -// /// Create new category from provided json string. -// /// -// /// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description", -// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", -// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", -// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", -// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names, -// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, -// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories -// /// "parentId" - id guid of category from which new branch of hierarchy will be built. -// /// Can be null if placed at the root of category tree. -// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant -// /// application this should be one hardcoded guid for whole app. -// /// -// /// -// /// -// /// -// /// (CategoryInstanceCreateRequest createRequest); ]]> -// /// -// /// This function will be called after deserializing the request from json -// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. -// /// -// /// -// /// -// public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( -// string categoryJsonString, -// Func>? requestDeserializedCallback = null, -// CancellationToken cancellationToken = default -// ) -// { -// JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); -// -// return CreateCategoryInstance( -// categoryJson.RootElement, -// requestDeserializedCallback, -// cancellationToken -// ); -// } -// -// /// -// /// Create new category from provided json string. -// /// -// /// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names. -// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, -// /// so they should not be in json. -// /// -// /// -// /// -// /// id of entity configuration which has all category attributes -// /// id of category tree, which represents separated hirerarchy with relations between categories -// /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. -// /// tenant id guid. A guid which uniquely identifies and isolates the data. For single -// /// tenant application this should be one hardcoded guid for whole app. -// /// -// /// (CategoryInstanceCreateRequest createRequest); ]]> -// /// -// /// This function will be called after deserializing the request from json -// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. -// /// -// /// -// /// -// public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( -// string categoryJsonString, -// Guid categoryConfigurationId, -// Guid categoryTreeId, -// Guid? parentId, -// Guid? tenantId, -// Func>? requestDeserializedCallback = null, -// CancellationToken cancellationToken = default -// ) -// { -// JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); -// -// return CreateCategoryInstance( -// categoryJson.RootElement, -// categoryConfigurationId, -// categoryTreeId, -// parentId, -// tenantId, -// requestDeserializedCallback, -// cancellationToken -// ); -// } -// -// /// -// /// Create new category from provided json document. -// /// -// /// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description", -// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", -// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", -// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", -// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names, -// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, -// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories -// /// "parentId" - id guid of category from which new branch of hierarchy will be built. -// /// Can be null if placed at the root of category tree. -// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant -// /// application this should be one hardcoded guid for whole app. -// /// -// /// -// /// -// /// -// /// (CategoryInstanceCreateRequest createRequest); ]]> -// /// -// /// This function will be called after deserializing the request from json -// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. -// /// -// /// -// /// -// public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( -// JsonElement categoryJson, -// Func>? requestDeserializedCallback = null, -// CancellationToken cancellationToken = default -// ) -// { -// var (categoryInstanceCreateRequest, deserializationErrors) = -// await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken); -// -// if (deserializationErrors != null) -// { -// return (null, deserializationErrors); -// } -// -// return await CreateCategoryInstance( -// categoryJson, -// categoryInstanceCreateRequest!.CategoryConfigurationId, -// categoryInstanceCreateRequest.CategoryTreeId, -// categoryInstanceCreateRequest.ParentId, -// categoryInstanceCreateRequest.TenantId, -// requestDeserializedCallback, -// cancellationToken -// ); -// } -// -// /// -// /// Create new category from provided json document. -// /// -// /// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names. -// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, -// /// so they should not be in json. -// /// -// /// -// /// -// /// id of entity configuration which has all category attributes -// /// id of category tree, which represents separated hirerarchy with relations between categories -// /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. -// /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single -// /// tenant application this should be one hardcoded guid for whole app. -// /// -// /// (CategoryInstanceCreateRequest createRequest); ]]> -// /// -// /// This function will be called after deserializing the request from json -// /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. -// /// -// /// -// /// -// public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( -// JsonElement categoryJson, -// Guid categoryConfigurationId, -// Guid categoryTreeId, -// Guid? parentId, -// Guid? tenantId, -// Func>? requestDeserializedCallback = null, -// CancellationToken cancellationToken = default -// ) -// { -// (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) -// = await DeserializeCategoryInstanceCreateRequestFromJson( -// categoryJson, -// categoryConfigurationId, -// categoryTreeId, -// parentId, -// tenantId, -// cancellationToken -// ); -// -// if (deserializationErrors != null) -// { -// return (null, deserializationErrors); -// } -// -// if (requestDeserializedCallback != null) -// { -// categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); -// } -// -// var (createdCategory, validationErrors) = await CreateCategoryInstance( -// categoryInstanceCreateRequest!, cancellationToken -// ); -// -// if (validationErrors != null) -// { -// return (null, validationErrors); -// } -// -// return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); -// } -// -// public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( -// CategoryInstanceCreateRequest categoryCreateRequest, -// CancellationToken cancellationToken = default -// ) -// { -// CategoryTree? tree = await _categoryTreeRepository.LoadAsync( -// categoryCreateRequest.CategoryTreeId, -// categoryCreateRequest.CategoryTreeId.ToString(), -// cancellationToken -// ).ConfigureAwait(false); -// -// if (tree == null) -// { -// return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; -// } -// -// if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) -// { -// return (null, -// new ValidationErrorResponse("CategoryConfigurationId", -// "Category tree uses another configuration for categories" -// ))!; -// } -// -// EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( -// categoryCreateRequest.CategoryConfigurationId, -// categoryCreateRequest.CategoryConfigurationId.ToString(), -// cancellationToken -// ).ConfigureAwait(false); -// -// -// if (entityConfiguration == null) -// { -// return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; -// } -// -// List attributeConfigurations = -// await GetAttributeConfigurationsForEntityConfiguration( -// entityConfiguration, -// cancellationToken -// ).ConfigureAwait(false); -// -// -// (var categoryPath, Guid? parentId, ProblemDetails? errors) = -// await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); -// -// if (errors != null) -// { -// return (null, errors)!; -// } -// -// var categoryInstance = new Category( -// Guid.NewGuid(), -// categoryCreateRequest.MachineName, -// categoryCreateRequest.CategoryConfigurationId, -// _mapper.Map>(categoryCreateRequest.Attributes), -// categoryCreateRequest.TenantId, -// categoryPath!, -// parentId, -// categoryCreateRequest.CategoryTreeId -// ); -// -// var validationErrors = new Dictionary(); -// foreach (AttributeConfiguration a in attributeConfigurations) -// { -// AttributeInstance? attributeValue = categoryInstance.Attributes -// .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); -// -// List attrValidationErrors = a.ValidateInstance(attributeValue); -// if (attrValidationErrors is { Count: > 0 }) -// { -// validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); -// } -// } -// -// if (validationErrors.Count > 0) -// { -// return (null, new ValidationErrorResponse(validationErrors))!; -// } -// -// ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory -// .FromEntityConfiguration(entityConfiguration, attributeConfigurations); -// -// IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); -// await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); -// -// var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) -// .ConfigureAwait(false); -// if (!saved) -// { -// //TODO: What do we want to do with internal exceptions and unsuccessful flow? -// throw new Exception("Entity was not saved"); -// } -// -// return (_mapper.Map(categoryInstance), null)!; -// } -// -// /// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description", -// /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", -// /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", -// /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", -// /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names, -// /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, -// /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories -// /// "parentId" - id guid of category from which new branch of hierarchy will be built. -// /// Can be null if placed at the root of category tree. -// /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant -// /// application this should be one hardcoded guid for whole app. -// /// -// /// -// public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( -// JsonElement categoryJson, -// CancellationToken cancellationToken = default -// ) -// { -// Guid categoryConfigurationId; -// if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) -// { -// if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) -// { -// categoryConfigurationId = categoryConfigurationIdGuid; -// } -// else -// { -// return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; -// } -// } -// else -// { -// return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); -// } -// -// Guid categoryTreeId; -// if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) -// { -// if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) -// { -// categoryTreeId = categoryTreeIdGuid; -// } -// else -// { -// return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; -// } -// } -// else -// { -// return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); -// } -// -// Guid? parentId = null; -// if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) -// { -// if (parentIdJsonElement.ValueKind == JsonValueKind.Null) -// { -// parentId = null; -// } -// else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) -// { -// parentId = parentIdGuid; -// } -// else -// { -// return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; -// } -// } -// -// Guid? tenantId = null; -// if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) -// { -// if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) -// { -// tenantId = null; -// } -// else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) -// { -// tenantId = tenantIdGuid; -// } -// else -// { -// return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; -// } -// } -// -// return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); -// } -// -// /// Use following json format: -// /// -// /// ``` -// /// { -// /// "name": "Main Category", -// /// "desprition": "Main Category description" -// /// } -// /// ``` -// /// -// /// Where "name" and "description" are attributes machine names. -// /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, -// /// so they should not be in json. -// /// -// /// -// public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( -// JsonElement categoryJson, -// Guid categoryConfigurationId, -// Guid categoryTreeId, -// Guid? parentId, -// Guid? tenantId, -// CancellationToken cancellationToken = default -// ) -// { -// EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( -// categoryConfigurationId, -// categoryConfigurationId.ToString(), -// cancellationToken -// ) -// .ConfigureAwait(false); -// -// if (categoryConfiguration == null) -// { -// return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; -// } -// -// List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( -// categoryConfiguration, -// cancellationToken -// ) -// .ConfigureAwait(false); -// -// return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( -// categoryConfigurationId, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson -// ); -// } -// -// /// -// /// Returns full category tree. -// /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. -// /// -// /// -// /// -// /// -// -// [SuppressMessage("Performance", "CA1806:Do not ignore method results")] -// public async Task> GetCategoryTreeViewAsync( -// Guid treeId, -// Guid? notDeeperThanCategoryId = null, -// CancellationToken cancellationToken = default -// ) -// { -// CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) -// .ConfigureAwait(false); -// if (tree == null) -// { -// throw new NotFoundException("Category tree not found"); -// } -// -// ProjectionQueryResult treeElementsQueryResult = -// await QueryInstances(tree.EntityConfigurationId, -// new ProjectionQuery -// { -// Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, -// Limit = _elasticSearchQueryOptions.MaxSize -// }, -// cancellationToken -// ).ConfigureAwait(false); -// -// var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); -// -// int searchedLevelPathLenght; -// -// if (notDeeperThanCategoryId != null) -// { -// var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); -// -// if (category == null) -// { -// throw new NotFoundException("Category not found"); -// } -// -// searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; -// -// treeElements = treeElements -// .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); -// } -// -// var treeViewModel = new List(); -// -// // Go through each instance once -// foreach (EntityInstanceViewModel treeElement in treeElements -// .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) -// { -// var treeElementViewModel = _mapper.Map(treeElement); -// var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; -// -// if (string.IsNullOrEmpty(categoryPath)) -// { -// treeViewModel.Add(treeElementViewModel); -// } -// else -// { -// IEnumerable categoryPathElements = -// categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); -// EntityTreeInstanceViewModel? currentLevel = null; -// categoryPathElements.Aggregate(treeViewModel, -// (acc, pathComponent) => -// { -// EntityTreeInstanceViewModel? parent = -// acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); -// if (parent == null) -// { -// EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); -// parent = _mapper.Map(parentInstance); -// acc.Add(parent); -// } -// -// currentLevel = parent; -// return parent.Children; -// } -// ); -// currentLevel?.Children.Add(treeElementViewModel); -// } -// } -// -// return treeViewModel; -// } -// -// /// -// /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. -// /// -// /// -// /// -// /// -// public async Task> GetSubcategories( -// Guid categoryTreeId, -// Guid? parentId, -// CancellationToken cancellationToken = default -// ) -// { -// var categoryTree = await _categoryTreeRepository.LoadAsync( -// categoryTreeId, categoryTreeId.ToString(), cancellationToken -// ).ConfigureAwait(false); -// -// if (categoryTree == null) -// { -// throw new NotFoundException("Category tree not found"); -// } -// -// var query = await GetSubcategoriesPrepareQuery(categoryTreeId, parentId, cancellationToken); -// -// var queryResult = _mapper.Map>( -// await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) -// ); -// -// return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); -// } -// -// private async Task GetSubcategoriesPrepareQuery( -// Guid categoryTreeId, -// Guid? parentId, -// CancellationToken cancellationToken = default -// ) -// { -// var categoryTree = await _categoryTreeRepository.LoadAsync( -// categoryTreeId, categoryTreeId.ToString(), cancellationToken -// ).ConfigureAwait(false); -// -// if (categoryTree == null) -// { -// throw new NotFoundException("Category tree not found"); -// } -// -// ProjectionQuery query = new ProjectionQuery -// { -// Limit = _elasticSearchQueryOptions.MaxSize -// }; -// -// if (parentId == null) -// { -// query.Filters.AddRange( -// -// new List -// { -// new Filter -// { -// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", -// Operator = FilterOperator.Equal, -// Value = categoryTree.Id.ToString(), -// }, -// new Filter -// { -// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", -// Operator = FilterOperator.Equal, -// Value = string.Empty -// } -// } -// ); -// -// return query; -// } -// -// var category = await _categoryInstanceRepository.LoadAsync( -// parentId.Value, categoryTree.EntityConfigurationId.ToString(), cancellationToken -// ).ConfigureAwait(false); -// -// if (category == null) -// { -// throw new NotFoundException("Category not found"); -// } -// -// string categoryPath = category.CategoryPaths.Where(x => x.TreeId == categoryTree.Id) -// .Select(p => p.Path).FirstOrDefault()!; -// -// query = new ProjectionQuery -// { -// Filters = new List -// { -// new Filter -// { -// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", -// Operator = FilterOperator.Equal, -// Value = categoryTree.Id.ToString(), -// }, -// new Filter -// { -// PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.Path)}", -// Operator = FilterOperator.Equal, -// Value = categoryPath + $"/{category.Id}" -// } -// } -// }; -// -// return query; -// } -// } +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +using AutoMapper; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Options; +using CloudFabric.EAV.Service.Serialization; +using CloudFabric.EventSourcing.Domain; +using CloudFabric.EventSourcing.EventStore; +using CloudFabric.EventSourcing.EventStore.Persistence; +using CloudFabric.Projections; +using CloudFabric.Projections.Queries; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using ProjectionDocumentSchemaFactory = + CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; + +namespace CloudFabric.EAV.Service; + +public class EAVCategoryService: EAVService +{ + + private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions; + + public EAVCategoryService(ILogger> logger, + IMapper mapper, + JsonSerializerOptions jsonSerializerOptions, + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionRepositoryFactory projectionRepositoryFactory, + EventUserInfo userInfo, + IOptions? elasticSearchQueryOptions = null) : base(logger, + new CategoryFromDictionaryDeserializer(mapper), + mapper, + jsonSerializerOptions, + aggregateRepositoryFactory, + projectionRepositoryFactory, + userInfo) + { + + _elasticSearchQueryOptions = elasticSearchQueryOptions != null + ? elasticSearchQueryOptions.Value + : new ElasticSearchQueryOptions(); + } + + + #region Categories + + public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( + CategoryTreeCreateRequest entity, + Guid? tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entity.EntityConfigurationId, + entity.EntityConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; + } + + var tree = new CategoryTree( + Guid.NewGuid(), + entity.EntityConfigurationId, + entity.MachineName, + tenantId + ); + + _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); + return (_mapper.Map(tree), null)!; + } + + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + string categoryJsonString, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return CreateCategoryInstance( + categoryJson.RootElement, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// + /// id of entity configuration which has all category attributes + /// id of category tree, which represents separated hirerarchy with relations between categories + /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. + /// tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + string categoryJsonString, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return CreateCategoryInstance( + categoryJson.RootElement, + machineName, + categoryConfigurationId, + categoryTreeId, + parentId, + tenantId, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + JsonElement categoryJson, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + var (categoryInstanceCreateRequest, deserializationErrors) = + await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await CreateCategoryInstance( + categoryJson, + categoryInstanceCreateRequest!.MachineName, + categoryInstanceCreateRequest!.CategoryConfigurationId, + categoryInstanceCreateRequest.CategoryTreeId, + categoryInstanceCreateRequest.ParentId, + categoryInstanceCreateRequest.TenantId, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// id of entity configuration which has all category attributes + /// id of category tree, which represents separated hirerarchy with relations between categories + /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (CategoryInstanceCreateRequest createRequest); ]]> + /// + /// This function will be called after deserializing the request from json + /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) + = await DeserializeCategoryInstanceCreateRequestFromJson( + categoryJson, + machineName, + categoryConfigurationId, + categoryTreeId, + parentId, + tenantId, + cancellationToken + ); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + if (requestDeserializedCallback != null) + { + categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); + } + + var (createdCategory, validationErrors) = await CreateCategoryInstance( + categoryInstanceCreateRequest!, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); + } + + public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( + CategoryInstanceCreateRequest categoryCreateRequest, + CancellationToken cancellationToken = default + ) + { + CategoryTree? tree = await _categoryTreeRepository.LoadAsync( + categoryCreateRequest.CategoryTreeId, + categoryCreateRequest.CategoryTreeId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (tree == null) + { + return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; + } + + if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) + { + return (null, + new ValidationErrorResponse("CategoryConfigurationId", + "Category tree uses another configuration for categories" + ))!; + } + + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + categoryCreateRequest.CategoryConfigurationId, + categoryCreateRequest.CategoryConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ).ConfigureAwait(false); + + + (var categoryPath, Guid? parentId, ProblemDetails? errors) = + await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); + + if (errors != null) + { + return (null, errors)!; + } + + var categoryInstance = new Category( + Guid.NewGuid(), + categoryCreateRequest.MachineName, + categoryCreateRequest.CategoryConfigurationId, + _mapper.Map>(categoryCreateRequest.Attributes), + categoryCreateRequest.TenantId, + categoryPath!, + parentId, + categoryCreateRequest.CategoryTreeId + ); + + var validationErrors = new Dictionary(); + foreach (AttributeConfiguration a in attributeConfigurations) + { + AttributeInstance? attributeValue = categoryInstance.Attributes + .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); + + List attrValidationErrors = a.ValidateInstance(attributeValue); + if (attrValidationErrors is { Count: > 0 }) + { + validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); + } + } + + if (validationErrors.Count > 0) + { + return (null, new ValidationErrorResponse(validationErrors))!; + } + + ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); + + IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); + await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); + + var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) + .ConfigureAwait(false); + if (!saved) + { + //TODO: What do we want to do with internal exceptions and unsuccessful flow? + throw new Exception("Entity was not saved"); + } + + return (_mapper.Map(categoryInstance), null)!; + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", + /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, + /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories + /// "parentId" - id guid of category from which new branch of hierarchy will be built. + /// Can be null if placed at the root of category tree. + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + JsonElement categoryJson, + CancellationToken cancellationToken = default + ) + { + Guid categoryConfigurationId; + if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) + { + if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) + { + categoryConfigurationId = categoryConfigurationIdGuid; + } + else + { + return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); + } + + Guid categoryTreeId; + if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) + { + if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) + { + categoryTreeId = categoryTreeIdGuid; + } + else + { + return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); + } + + Guid? parentId = null; + if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) + { + if (parentIdJsonElement.ValueKind == JsonValueKind.Null) + { + parentId = null; + } + else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) + { + parentId = parentIdGuid; + } + else + { + return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; + } + } + + Guid? tenantId = null; + if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) + { + if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) + { + tenantId = null; + } + else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) + { + tenantId = tenantIdGuid; + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; + } + } + + string? machineName = null; + if (categoryJson.TryGetProperty("machineName", out var machineNameJsonElement)) + { + machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString(); + if (machineName == null) + { + return (null, new ValidationErrorResponse("machineName", "Value is not a valid")); + } + } + + return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); + } + + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "desprition": "Main Category description" + /// } + /// ``` + /// + /// Where "name" and "description" are attributes machine names. + /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( + categoryConfigurationId, + categoryConfigurationId.ToString(), + cancellationToken + ) + .ConfigureAwait(false); + + if (categoryConfiguration == null) + { + return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; + } + + List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( + categoryConfiguration, + cancellationToken + ) + .ConfigureAwait(false); + + return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( + categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson + ); + } + + /// + /// Returns full category tree. + /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. + /// + /// + /// + /// + + [SuppressMessage("Performance", "CA1806:Do not ignore method results")] + public async Task> GetCategoryTreeViewAsync( + Guid treeId, + Guid? notDeeperThanCategoryId = null, + CancellationToken cancellationToken = default + ) + { + CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) + .ConfigureAwait(false); + if (tree == null) + { + throw new NotFoundException("Category tree not found"); + } + + ProjectionQueryResult treeElementsQueryResult = + await QueryInstances(tree.EntityConfigurationId, + new ProjectionQuery + { + Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, + Limit = _elasticSearchQueryOptions.MaxSize + }, + cancellationToken + ).ConfigureAwait(false); + + var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); + + int searchedLevelPathLenght; + + if (notDeeperThanCategoryId != null) + { + var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); + + if (category == null) + { + throw new NotFoundException("Category not found"); + } + + searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; + + treeElements = treeElements + .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); + } + + var treeViewModel = new List(); + + // Go through each instance once + foreach (CategoryViewModel treeElement in treeElements + .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) + { + var treeElementViewModel = _mapper.Map(treeElement); + var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; + + if (string.IsNullOrEmpty(categoryPath)) + { + treeViewModel.Add(treeElementViewModel); + } + else + { + IEnumerable categoryPathElements = + categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); + EntityTreeInstanceViewModel? currentLevel = null; + categoryPathElements.Aggregate(treeViewModel, + (acc, pathComponent) => + { + EntityTreeInstanceViewModel? parent = + acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); + if (parent == null) + { + EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); + parent = _mapper.Map(parentInstance); + acc.Add(parent); + } + + currentLevel = parent; + return parent.Children; + } + ); + currentLevel?.Children.Add(treeElementViewModel); + } + } + + return treeViewModel; + } + + /// + /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. + /// + /// + /// + /// + public async Task> GetSubcategories( + Guid categoryTreeId, + Guid? parentId = null, + string? parentMachineName = null, + CancellationToken cancellationToken = default + ) + { + var categoryTree = await _categoryTreeRepository.LoadAsync( + categoryTreeId, categoryTreeId.ToString(), cancellationToken + ).ConfigureAwait(false); + + if (categoryTree == null) + { + throw new NotFoundException("Category tree not found"); + } + + var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken); + + var queryResult = _mapper.Map>( + await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) + ); + + return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); + } + + private ProjectionQuery GetSubcategoriesPrepareQuery( + CategoryTree categoryTree, + Guid? parentId, + string? parentMachineName, + CancellationToken cancellationToken = default + ) + { + ProjectionQuery query = new ProjectionQuery + { + Limit = _elasticSearchQueryOptions.MaxSize + }; + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", + Operator = FilterOperator.Equal, + Value = categoryTree.Id.ToString(), + }); + + // If nothing is set - get subcategories of master level + if (parentId == null && string.IsNullOrEmpty(parentMachineName)) + { + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = string.Empty, + }); + return query; + } + + if (parentId != null) + { + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", + Operator = FilterOperator.Equal, + Value = parentId.ToString() + }); + } + + if (!string.IsNullOrEmpty(parentMachineName)) + { + + query.Filters.Add(new Filter + { + PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + Operator = FilterOperator.Equal, + Value = parentMachineName + }); + } + return query; + + } + + #endregion + +} diff --git a/CloudFabric.EAV.Service/EAVEntityInstanceService.cs b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs new file mode 100644 index 0000000..dd0783e --- /dev/null +++ b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs @@ -0,0 +1,533 @@ +using System.Text.Json; + +using AutoMapper; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Domain.Models.Attributes; +using CloudFabric.EAV.Enums; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Options; +using CloudFabric.EAV.Service.Serialization; +using CloudFabric.EventSourcing.Domain; +using CloudFabric.EventSourcing.EventStore; +using CloudFabric.EventSourcing.EventStore.Persistence; +using CloudFabric.Projections; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using ProjectionDocumentSchemaFactory = + CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; +namespace CloudFabric.EAV.Service; + +public class EAVEntityInstanceService: EAVService +{ + + public EAVEntityInstanceService(ILogger> logger, + IMapper mapper, + JsonSerializerOptions jsonSerializerOptions, + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionRepositoryFactory projectionRepositoryFactory, + EventUserInfo userInfo) : base(logger, + new EntityInstanceFromDictionaryDeserializer(mapper), + mapper, + jsonSerializerOptions, + aggregateRepositoryFactory, + projectionRepositoryFactory, + userInfo) + { + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + JsonElement entityJson, + CancellationToken cancellationToken = default + ) + { + Guid entityConfigurationId; + if (entityJson.TryGetProperty("entityConfigurationId", out var entityConfigurationIdJsonElement)) + { + if (entityConfigurationIdJsonElement.TryGetGuid(out var entityConfigurationIdGuid)) + { + entityConfigurationId = entityConfigurationIdGuid; + } + else + { + return (null, new ValidationErrorResponse("entityConfigurationId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("entityConfigurationId", "Value is missing")); + } + + Guid tenantId; + if (entityJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) + { + if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) + { + tenantId = tenantIdGuid; + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; + } + } + else + { + return (null, new ValidationErrorResponse("tenantId", "Value is missing")); + } + + return await DeserializeEntityInstanceCreateRequestFromJson( + entityJson, entityConfigurationId, tenantId, cancellationToken + ); + } + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entityConfigurationId, + entityConfigurationId.ToString(), + cancellationToken + ) + .ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ) + .ConfigureAwait(false); + + return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeEntityInstanceCreateRequest( + entityConfigurationId, tenantId, attributeConfigurations, entityJson + ); + } + + /// + /// Create new entity instance from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + string entityJsonString, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return CreateEntityInstance( + entityJson.RootElement, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// Id of entity configuration which has all attributes + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + string entityJsonString, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return CreateEntityInstance( + entityJson.RootElement, + entityConfigurationId, + tenantId, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity", + /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", + /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" + /// } + /// ``` + /// + /// Where "sku" and "name" are attributes machine names, + /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, + /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant + /// application this should be one hardcoded guid for whole app. + /// + /// + /// + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + JsonElement entityJson, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = + await DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await CreateEntityInstance( + entityJson, + // Deserialization method ensures that EntityConfigurationId and TenantId exist and returns errors if not + // so it's safe to use ! here + entityInstanceCreateRequest!.EntityConfigurationId, + entityInstanceCreateRequest.TenantId!.Value, + requestDeserializedCallback, + dryRun, + requiredAttributesCanBeNull, + cancellationToken + ); + } + + /// + /// Create new entity instance from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "sku": "123", + /// "name": "New Entity" + /// } + /// ``` + /// + /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, + /// so they should not be in json. + /// + /// + /// + /// Id of entity configuration which has all attributes + /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single + /// tenant application this should be one hardcoded guid for whole app. + /// + /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> + /// + /// This function will be called after deserializing the request from json + /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. + /// + /// Note that it's important to check dryRun parameter and not make any changes to persistent store if + /// the parameter equals to 'true'. + /// + /// If true, entity will only be validated but not saved to the database + /// + /// + public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = await + DeserializeEntityInstanceCreateRequestFromJson( + entityJson, entityConfigurationId, tenantId, cancellationToken + ); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + if (requestDeserializedCallback != null) + { + entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun); + } + + var (createdEntity, validationErrors) = await CreateEntityInstance( + entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null); + } + + + + public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance( + EntityInstanceCreateRequest entity, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + entity.EntityConfigurationId, + entity.EntityConfigurationId.ToString(), + cancellationToken + ).ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; + } + + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ).ConfigureAwait(false); + + //TODO: add check for categoryPath + var entityInstance = new EntityInstance( + Guid.NewGuid(), + entity.EntityConfigurationId, + _mapper.Map>(entity.Attributes), + entity.TenantId + ); + + var validationErrors = new Dictionary(); + foreach (AttributeConfiguration a in attributeConfigurations) + { + AttributeInstance? attributeValue = entityInstance.Attributes + .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); + + List attrValidationErrors = a.ValidateInstance(attributeValue, requiredAttributesCanBeNull); + if (attrValidationErrors is { Count: > 0 }) + { + validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); + } + + // Note that this method updates entityConfiguration state (for serial attribute it increments the number + // stored in externalvalues) but does not save entity configuration, we need to do that manually outside of + // the loop + InitializeAttributeInstanceWithExternalValuesFromEntity(entityConfiguration, a, attributeValue); + } + + if (validationErrors.Count > 0) + { + return (null, new ValidationErrorResponse(validationErrors))!; + } + + if (!dryRun) + { + var entityConfigurationSaved = await _entityConfigurationRepository + .SaveAsync(_userInfo, entityConfiguration, cancellationToken) + .ConfigureAwait(false); + + if (!entityConfigurationSaved) + { + throw new Exception("Entity was not saved"); + } + + ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); + + IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); + await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); + + var entityInstanceSaved = + await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken); + + if (!entityInstanceSaved) + { + //TODO: What do we want to do with internal exceptions and unsuccessful flow? + throw new Exception("Entity was not saved"); + } + + return (_mapper.Map(entityInstance), null); + } + + return (_mapper.Map(entityInstance), null); + } + + private void InitializeAttributeInstanceWithExternalValuesFromEntity( + EntityConfiguration entityConfiguration, + AttributeConfiguration attributeConfiguration, + AttributeInstance? attributeInstance + ) + { + switch (attributeConfiguration.ValueType) + { + case EavAttributeType.Serial: + { + if (attributeInstance == null) + { + return; + } + + var serialAttributeConfiguration = attributeConfiguration as SerialAttributeConfiguration; + + var serialInstance = attributeInstance as SerialAttributeInstance; + + if (serialAttributeConfiguration == null || serialInstance == null) + { + throw new ArgumentException("Invalid attribute type"); + } + + EntityConfigurationAttributeReference? entityAttribute = entityConfiguration.Attributes + .FirstOrDefault(x => x.AttributeConfigurationId == attributeConfiguration.Id); + + if (entityAttribute == null) + { + throw new NotFoundException("Attribute not found"); + } + + var existingAttributeValue = + entityAttribute.AttributeConfigurationExternalValues.FirstOrDefault(); + + long? deserializedValue = null; + + if (existingAttributeValue != null) + { + deserializedValue = JsonSerializer.Deserialize(existingAttributeValue.ToString()!); + } + + var newExternalValue = existingAttributeValue == null + ? serialAttributeConfiguration.StartingNumber + : deserializedValue += serialAttributeConfiguration.Increment; + + serialInstance.Value = newExternalValue!.Value; + + entityConfiguration.UpdateAttrributeExternalValues(attributeConfiguration.Id, + new List { newExternalValue } + ); + } + break; + } + } +} diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 4b55f01..4a0178c 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; @@ -15,7 +14,6 @@ using CloudFabric.EAV.Models.RequestModels.Attributes; using CloudFabric.EAV.Models.ViewModels; using CloudFabric.EAV.Models.ViewModels.Attributes; -using CloudFabric.EAV.Options; using CloudFabric.EAV.Service.Serialization; using CloudFabric.EventSourcing.Domain; using CloudFabric.EventSourcing.EventStore; @@ -25,51 +23,51 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using ProjectionDocumentSchemaFactory = CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory; namespace CloudFabric.EAV.Service; -public class EAVService : IEAVService +public class EAVService where V: EntityInstanceViewModel + where U: EntityInstanceUpdateRequest + where T: EntityInstanceBase { - private readonly AggregateRepositoryFactory _aggregateRepositoryFactory; + internal readonly AggregateRepositoryFactory _aggregateRepositoryFactory; private readonly IProjectionRepository _attributeConfigurationProjectionRepository; private readonly AggregateRepository _attributeConfigurationRepository; - private readonly AggregateRepository _categoryTreeRepository; private readonly IProjectionRepository _entityConfigurationProjectionRepository; - private readonly AggregateRepository _entityConfigurationRepository; + internal readonly AggregateRepository _entityConfigurationRepository; - private readonly EntityInstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; + private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; - private readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer + internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer _entityInstanceCreateUpdateRequestFromJsonDeserializer; - private readonly AggregateRepository _entityInstanceRepository; - private readonly AggregateRepository _categoryInstanceRepository; - private readonly ILogger _logger; - private readonly IMapper _mapper; + internal readonly AggregateRepository _entityInstanceRepository; + private readonly ILogger> _logger; + internal readonly IMapper _mapper; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory; - private readonly EventUserInfo _userInfo; + internal readonly EventUserInfo _userInfo; - private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions; + internal readonly AggregateRepository _categoryTreeRepository; + internal readonly AggregateRepository _categoryInstanceRepository; public EAVService( - ILogger logger, + ILogger> logger, + InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer, IMapper mapper, JsonSerializerOptions jsonSerializerOptions, AggregateRepositoryFactory aggregateRepositoryFactory, ProjectionRepositoryFactory projectionRepositoryFactory, - EventUserInfo userInfo, - IOptions? elasticSearchQueryOptions = null + EventUserInfo userInfo ) { _logger = logger; @@ -81,32 +79,32 @@ public EAVService( _userInfo = userInfo; - _elasticSearchQueryOptions = elasticSearchQueryOptions != null - ? elasticSearchQueryOptions.Value - : new ElasticSearchQueryOptions(); + _attributeConfigurationRepository = _aggregateRepositoryFactory .GetAggregateRepository(); _entityConfigurationRepository = _aggregateRepositoryFactory .GetAggregateRepository(); _entityInstanceRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); - _categoryInstanceRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); - _categoryTreeRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); + .GetAggregateRepository(); + _attributeConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); _entityConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); - _entityInstanceFromDictionaryDeserializer = new EntityInstanceFromDictionaryDeserializer(_mapper); + _entityInstanceFromDictionaryDeserializer = instanceFromDictionaryDeserializer; _entityInstanceCreateUpdateRequestFromJsonDeserializer = new EntityInstanceCreateUpdateRequestFromJsonDeserializer( _attributeConfigurationRepository, jsonSerializerOptions ); + + _categoryInstanceRepository = _aggregateRepositoryFactory + .GetAggregateRepository(); + _categoryTreeRepository = _aggregateRepositoryFactory + .GetAggregateRepository(); } private void EnsureAttributeMachineNameIsAdded(AttributeConfigurationCreateUpdateRequest attributeRequest) @@ -188,61 +186,7 @@ private async Task CheckAttributesListMachineNameUnique( return true; } - private void InitializeAttributeInstanceWithExternalValuesFromEntity( - EntityConfiguration entityConfiguration, - AttributeConfiguration attributeConfiguration, - AttributeInstance? attributeInstance - ) - { - switch (attributeConfiguration.ValueType) - { - case EavAttributeType.Serial: - { - if (attributeInstance == null) - { - return; - } - - var serialAttributeConfiguration = attributeConfiguration as SerialAttributeConfiguration; - - var serialInstance = attributeInstance as SerialAttributeInstance; - - if (serialAttributeConfiguration == null || serialInstance == null) - { - throw new ArgumentException("Invalid attribute type"); - } - - EntityConfigurationAttributeReference? entityAttribute = entityConfiguration.Attributes - .FirstOrDefault(x => x.AttributeConfigurationId == attributeConfiguration.Id); - - if (entityAttribute == null) - { - throw new NotFoundException("Attribute not found"); - } - - var existingAttributeValue = - entityAttribute.AttributeConfigurationExternalValues.FirstOrDefault(); - - long? deserializedValue = null; - - if (existingAttributeValue != null) - { - deserializedValue = JsonSerializer.Deserialize(existingAttributeValue.ToString()!); - } - - var newExternalValue = existingAttributeValue == null - ? serialAttributeConfiguration.StartingNumber - : deserializedValue += serialAttributeConfiguration.Increment; - serialInstance.Value = newExternalValue!.Value; - - entityConfiguration.UpdateAttrributeExternalValues(attributeConfiguration.Id, - new List { newExternalValue } - ); - } - break; - } - } private async Task> GetAttributesByIds( List attributesIds, CancellationToken cancellationToken) @@ -274,7 +218,7 @@ private async Task BuildCategoryPath(Guid treeId, Guid? parentId, + internal async Task<(string?, Guid?, ProblemDetails?)> BuildCategoryPath(Guid treeId, Guid? parentId, CancellationToken cancellationToken) { CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) @@ -996,1160 +940,18 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR // return _mapper.Map>(records); // } - #region Categories - - public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( - CategoryTreeCreateRequest entity, - Guid? tenantId, - CancellationToken cancellationToken = default - ) - { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entity.EntityConfigurationId, - entity.EntityConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; - } - - var tree = new CategoryTree( - Guid.NewGuid(), - entity.EntityConfigurationId, - entity.MachineName, - tenantId - ); - - _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false); - return (_mapper.Map(tree), null)!; - } - - /// - /// Create new category from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - string categoryJsonString, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) - { - JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); - - return CreateCategoryInstance( - categoryJson.RootElement, - requestDeserializedCallback, - cancellationToken - ); - } - - /// - /// Create new category from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// - /// id of entity configuration which has all category attributes - /// id of category tree, which represents separated hirerarchy with relations between categories - /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. - /// tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - string categoryJsonString, - string machineName, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) - { - JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); - - return CreateCategoryInstance( - categoryJson.RootElement, - machineName, - categoryConfigurationId, - categoryTreeId, - parentId, - tenantId, - requestDeserializedCallback, - cancellationToken - ); - } - - /// - /// Create new category from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - JsonElement categoryJson, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) - { - var (categoryInstanceCreateRequest, deserializationErrors) = - await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - return await CreateCategoryInstance( - categoryJson, - categoryInstanceCreateRequest!.MachineName, - categoryInstanceCreateRequest!.CategoryConfigurationId, - categoryInstanceCreateRequest.CategoryTreeId, - categoryInstanceCreateRequest.ParentId, - categoryInstanceCreateRequest.TenantId, - requestDeserializedCallback, - cancellationToken - ); - } - - /// - /// Create new category from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// id of entity configuration which has all category attributes - /// id of category tree, which represents separated hirerarchy with relations between categories - /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree. - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (CategoryInstanceCreateRequest createRequest); ]]> - /// - /// This function will be called after deserializing the request from json - /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( - JsonElement categoryJson, - string machineName, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - Func>? requestDeserializedCallback = null, - CancellationToken cancellationToken = default - ) - { - (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) - = await DeserializeCategoryInstanceCreateRequestFromJson( - categoryJson, - machineName, - categoryConfigurationId, - categoryTreeId, - parentId, - tenantId, - cancellationToken - ); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - if (requestDeserializedCallback != null) - { - categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); - } - - var (createdCategory, validationErrors) = await CreateCategoryInstance( - categoryInstanceCreateRequest!, cancellationToken - ); - - if (validationErrors != null) - { - return (null, validationErrors); - } - - return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); - } - - public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( - CategoryInstanceCreateRequest categoryCreateRequest, - CancellationToken cancellationToken = default - ) - { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync( - categoryCreateRequest.CategoryTreeId, - categoryCreateRequest.CategoryTreeId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (tree == null) - { - return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!; - } - - if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId) - { - return (null, - new ValidationErrorResponse("CategoryConfigurationId", - "Category tree uses another configuration for categories" - ))!; - } - - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - categoryCreateRequest.CategoryConfigurationId, - categoryCreateRequest.CategoryConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!; - } - - List attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ).ConfigureAwait(false); - - - (var categoryPath, Guid? parentId, ProblemDetails? errors) = - await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); - - if (errors != null) - { - return (null, errors)!; - } - - var categoryInstance = new Category( - Guid.NewGuid(), - categoryCreateRequest.MachineName, - categoryCreateRequest.CategoryConfigurationId, - _mapper.Map>(categoryCreateRequest.Attributes), - categoryCreateRequest.TenantId, - categoryPath!, - parentId, - categoryCreateRequest.CategoryTreeId - ); - - var validationErrors = new Dictionary(); - foreach (AttributeConfiguration a in attributeConfigurations) - { - AttributeInstance? attributeValue = categoryInstance.Attributes - .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); - - List attrValidationErrors = a.ValidateInstance(attributeValue); - if (attrValidationErrors is { Count: > 0 }) - { - validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); - } - } - - if (validationErrors.Count > 0) - { - return (null, new ValidationErrorResponse(validationErrors))!; - } - - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory - .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - - IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); - await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - - var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) - .ConfigureAwait(false); - if (!saved) - { - //TODO: What do we want to do with internal exceptions and unsuccessful flow? - throw new Exception("Entity was not saved"); - } - - return (_mapper.Map(categoryInstance), null)!; - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4", - /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes, - /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories - /// "parentId" - id guid of category from which new branch of hierarchy will be built. - /// Can be null if placed at the root of category tree. - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( - JsonElement categoryJson, - CancellationToken cancellationToken = default - ) - { - Guid categoryConfigurationId; - if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement)) - { - if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid)) - { - categoryConfigurationId = categoryConfigurationIdGuid; - } - else - { - return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing")); - } - - Guid categoryTreeId; - if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement)) - { - if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid)) - { - categoryTreeId = categoryTreeIdGuid; - } - else - { - return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing")); - } - - Guid? parentId = null; - if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement)) - { - if (parentIdJsonElement.ValueKind == JsonValueKind.Null) - { - parentId = null; - } - else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid)) - { - parentId = parentIdGuid; - } - else - { - return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!; - } - } - - Guid? tenantId = null; - if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) - { - if (tenantIdJsonElement.ValueKind == JsonValueKind.Null) - { - tenantId = null; - } - else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) - { - tenantId = tenantIdGuid; - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; - } - } - - string? machineName = null; - if (categoryJson.TryGetProperty("machineName", out var machineNameJsonElement)) - { - machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString(); - if (machineName == null) - { - return (null, new ValidationErrorResponse("machineName", "Value is not a valid")); - } - } - - return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); - } - - /// Use following json format: - /// - /// ``` - /// { - /// "name": "Main Category", - /// "desprition": "Main Category description" - /// } - /// ``` - /// - /// Where "name" and "description" are attributes machine names. - /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( - JsonElement categoryJson, - string machineName, - Guid categoryConfigurationId, - Guid categoryTreeId, - Guid? parentId, - Guid? tenantId, - CancellationToken cancellationToken = default - ) - { - EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync( - categoryConfigurationId, - categoryConfigurationId.ToString(), - cancellationToken - ) - .ConfigureAwait(false); - - if (categoryConfiguration == null) - { - return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; - } - - List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( - categoryConfiguration, - cancellationToken - ) - .ConfigureAwait(false); - - return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( - categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson - ); - } - - /// - /// Returns full category tree. - /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided. - /// - /// - /// - /// - - [SuppressMessage("Performance", "CA1806:Do not ignore method results")] - public async Task> GetCategoryTreeViewAsync( - Guid treeId, - Guid? notDeeperThanCategoryId = null, - CancellationToken cancellationToken = default - ) - { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) - .ConfigureAwait(false); - if (tree == null) - { - throw new NotFoundException("Category tree not found"); - } - - ProjectionQueryResult treeElementsQueryResult = - await QueryInstances(tree.EntityConfigurationId, - new ProjectionQuery - { - Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) }, - Limit = _elasticSearchQueryOptions.MaxSize - }, - cancellationToken - ).ConfigureAwait(false); - - var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); - - int searchedLevelPathLenght; - - if (notDeeperThanCategoryId != null) - { - var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); - - if (category == null) - { - throw new NotFoundException("Category not found"); - } - - searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; - - treeElements = treeElements - .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); - } - - var treeViewModel = new List(); - - // Go through each instance once - foreach (EntityInstanceViewModel treeElement in treeElements - .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) - { - var treeElementViewModel = _mapper.Map(treeElement); - var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; - - if (string.IsNullOrEmpty(categoryPath)) - { - treeViewModel.Add(treeElementViewModel); - } - else - { - IEnumerable categoryPathElements = - categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); - EntityTreeInstanceViewModel? currentLevel = null; - categoryPathElements.Aggregate(treeViewModel, - (acc, pathComponent) => - { - EntityTreeInstanceViewModel? parent = - acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); - if (parent == null) - { - EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); - parent = _mapper.Map(parentInstance); - acc.Add(parent); - } - - currentLevel = parent; - return parent.Children; - } - ); - currentLevel?.Children.Add(treeElementViewModel); - } - } - - return treeViewModel; - } - - /// - /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. - /// - /// - /// - /// - public async Task> GetSubcategories( - Guid categoryTreeId, - Guid? parentId = null, - string? parentMachineName = null, - CancellationToken cancellationToken = default - ) - { - var categoryTree = await _categoryTreeRepository.LoadAsync( - categoryTreeId, categoryTreeId.ToString(), cancellationToken - ).ConfigureAwait(false); - - if (categoryTree == null) - { - throw new NotFoundException("Category tree not found"); - } - - var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken); - - var queryResult = _mapper.Map>( - await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) - ); - - return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); - } - - private ProjectionQuery GetSubcategoriesPrepareQuery( - CategoryTree categoryTree, - Guid? parentId, - string? parentMachineName, - CancellationToken cancellationToken = default - ) - { - ProjectionQuery query = new ProjectionQuery - { - Limit = _elasticSearchQueryOptions.MaxSize - }; - - query.Filters.Add(new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", - Operator = FilterOperator.Equal, - Value = categoryTree.Id.ToString(), - }); - - // If nothing is set - get subcategories of master level - if (parentId == null && string.IsNullOrEmpty(parentMachineName)) - { - query.Filters.Add(new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", - Operator = FilterOperator.Equal, - Value = string.Empty, - }); - return query; - } - - if (parentId != null) - { - - query.Filters.Add(new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", - Operator = FilterOperator.Equal, - Value = parentId.ToString() - }); - } - - if (!string.IsNullOrEmpty(parentMachineName)) - { - - query.Filters.Add(new Filter - { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", - Operator = FilterOperator.Equal, - Value = parentMachineName - }); - } - return query; - - } - - #endregion - #region EntityInstance - /// - /// Create new entity instance from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - string entityJsonString, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - JsonDocument entityJson = JsonDocument.Parse(entityJsonString); - - return CreateEntityInstance( - entityJson.RootElement, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json string. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// Id of entity configuration which has all attributes - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - string entityJsonString, - Guid entityConfigurationId, - Guid tenantId, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - JsonDocument entityJson = JsonDocument.Parse(entityJsonString); - - return CreateEntityInstance( - entityJson.RootElement, - entityConfigurationId, - tenantId, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - /// - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - JsonElement entityJson, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - var (entityInstanceCreateRequest, deserializationErrors) = - await DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - return await CreateEntityInstance( - entityJson, - // Deserialization method ensures that EntityConfigurationId and TenantId exist and returns errors if not - // so it's safe to use ! here - entityInstanceCreateRequest!.EntityConfigurationId, - entityInstanceCreateRequest.TenantId!.Value, - requestDeserializedCallback, - dryRun, - requiredAttributesCanBeNull, - cancellationToken - ); - } - - /// - /// Create new entity instance from provided json document. - /// - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - /// - /// Id of entity configuration which has all attributes - /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single - /// tenant application this should be one hardcoded guid for whole app. - /// - /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]> - /// - /// This function will be called after deserializing the request from json - /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic. - /// - /// Note that it's important to check dryRun parameter and not make any changes to persistent store if - /// the parameter equals to 'true'. - /// - /// If true, entity will only be validated but not saved to the database - /// - /// - public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( - JsonElement entityJson, - Guid entityConfigurationId, - Guid tenantId, - Func>? requestDeserializedCallback = null, - bool dryRun = false, - bool requiredAttributesCanBeNull = false, - CancellationToken cancellationToken = default - ) - { - var (entityInstanceCreateRequest, deserializationErrors) = await - DeserializeEntityInstanceCreateRequestFromJson( - entityJson, entityConfigurationId, tenantId, cancellationToken - ); - - if (deserializationErrors != null) - { - return (null, deserializationErrors); - } - - if (requestDeserializedCallback != null) - { - entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun); - } - - var (createdEntity, validationErrors) = await CreateEntityInstance( - entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken - ); - - if (validationErrors != null) - { - return (null, validationErrors); - } - - return (SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null); - } - - public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance( - EntityInstanceCreateRequest entity, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default - ) - { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entity.EntityConfigurationId, - entity.EntityConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; - } - - List attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ).ConfigureAwait(false); - - //TODO: add check for categoryPath - var entityInstance = new EntityInstance( - Guid.NewGuid(), - entity.EntityConfigurationId, - _mapper.Map>(entity.Attributes), - entity.TenantId - ); - - var validationErrors = new Dictionary(); - foreach (AttributeConfiguration a in attributeConfigurations) - { - AttributeInstance? attributeValue = entityInstance.Attributes - .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName); - - List attrValidationErrors = a.ValidateInstance(attributeValue, requiredAttributesCanBeNull); - if (attrValidationErrors is { Count: > 0 }) - { - validationErrors.Add(a.MachineName, attrValidationErrors.ToArray()); - } - - // Note that this method updates entityConfiguration state (for serial attribute it increments the number - // stored in externalvalues) but does not save entity configuration, we need to do that manually outside of - // the loop - InitializeAttributeInstanceWithExternalValuesFromEntity(entityConfiguration, a, attributeValue); - } - - if (validationErrors.Count > 0) - { - return (null, new ValidationErrorResponse(validationErrors))!; - } - - if (!dryRun) - { - var entityConfigurationSaved = await _entityConfigurationRepository - .SaveAsync(_userInfo, entityConfiguration, cancellationToken) - .ConfigureAwait(false); - - if (!entityConfigurationSaved) - { - throw new Exception("Entity was not saved"); - } - - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory - .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - - IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); - await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - - var entityInstanceSaved = - await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken); - - if (!entityInstanceSaved) - { - //TODO: What do we want to do with internal exceptions and unsuccessful flow? - throw new Exception("Entity was not saved"); - } - - return (_mapper.Map(entityInstance), null); - } - - return (_mapper.Map(entityInstance), null); - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity", - /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7", - /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82" - /// } - /// ``` - /// - /// Where "sku" and "name" are attributes machine names, - /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes, - /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant - /// application this should be one hardcoded guid for whole app. - /// - /// - public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( - JsonElement entityJson, - CancellationToken cancellationToken = default - ) - { - Guid entityConfigurationId; - if (entityJson.TryGetProperty("entityConfigurationId", out var entityConfigurationIdJsonElement)) - { - if (entityConfigurationIdJsonElement.TryGetGuid(out var entityConfigurationIdGuid)) - { - entityConfigurationId = entityConfigurationIdGuid; - } - else - { - return (null, new ValidationErrorResponse("entityConfigurationId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("entityConfigurationId", "Value is missing")); - } - - Guid tenantId; - if (entityJson.TryGetProperty("tenantId", out var tenantIdJsonElement)) - { - if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid)) - { - tenantId = tenantIdGuid; - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!; - } - } - else - { - return (null, new ValidationErrorResponse("tenantId", "Value is missing")); - } - - return await DeserializeEntityInstanceCreateRequestFromJson( - entityJson, entityConfigurationId, tenantId, cancellationToken - ); - } - - /// - /// Use following json format: - /// - /// ``` - /// { - /// "sku": "123", - /// "name": "New Entity" - /// } - /// ``` - /// - /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments, - /// so they should not be in json. - /// - /// - public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( - JsonElement entityJson, - Guid entityConfigurationId, - Guid tenantId, - CancellationToken cancellationToken = default - ) - { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entityConfigurationId, - entityConfigurationId.ToString(), - cancellationToken - ) - .ConfigureAwait(false); - - if (entityConfiguration == null) - { - return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!; - } - - List attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ) - .ConfigureAwait(false); - - return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeEntityInstanceCreateRequest( - entityConfigurationId, tenantId, attributeConfigurations, entityJson - ); - } - - public async Task GetEntityInstance(Guid id, string partitionKey) + public async Task GetEntityInstance(Guid id, string partitionKey) { - EntityInstance? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); + T? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); - return _mapper.Map(entityInstance); + return _mapper.Map(entityInstance); } public async Task GetEntityInstanceJsonMultiLanguage(Guid id, string partitionKey) { - EntityInstanceViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + V? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); return SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); } @@ -2160,12 +962,12 @@ public async Task GetEntityInstanceJsonSingleLanguage( string language, string fallbackLanguage = "en-US") { - EntityInstanceViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + V? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); return SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); } - public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(EntityInstanceViewModel? entityInstanceViewModel) + public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(V? entityInstanceViewModel) { var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); @@ -2175,7 +977,7 @@ public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(EntityInstanceVie } public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( - EntityInstanceViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" + V? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" ) { var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); @@ -2185,10 +987,10 @@ public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); } - public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateEntityInstance(string partitionKey, - EntityInstanceUpdateRequest updateRequest, CancellationToken cancellationToken) + public async Task<(V, ProblemDetails)> UpdateEntityInstance(string partitionKey, + U updateRequest, CancellationToken cancellationToken) { - EntityInstance? entityInstance = + T? entityInstance = await _entityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); if (entityInstance == null) @@ -2290,7 +1092,7 @@ await GetAttributeConfigurationsForEntityConfiguration( //TODO: Throw a error when ready } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } /// @@ -2302,7 +1104,7 @@ await GetAttributeConfigurationsForEntityConfiguration( /// /// /// - public async Task> QueryInstances( + public async Task> QueryInstances( Guid entityConfigurationId, ProjectionQuery query, CancellationToken cancellationToken = default @@ -2427,10 +1229,10 @@ public async Task> QueryInstancesJsonSingleL ); } - public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, + public async Task<(V, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, string entityInstancePartitionKey, Guid treeId, Guid? newParentId, CancellationToken cancellationToken = default) { - EntityInstance? entityInstance = await _entityInstanceRepository + T? entityInstance = await _entityInstanceRepository .LoadAsync(entityInstanceId, entityInstancePartitionKey, cancellationToken).ConfigureAwait(false); if (entityInstance == null) { @@ -2454,10 +1256,10 @@ public async Task> QueryInstancesJsonSingleL throw new Exception("Entity was not saved"); } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } - private async Task> GetAttributeConfigurationsForEntityConfiguration( + internal async Task> GetAttributeConfigurationsForEntityConfiguration( EntityConfiguration entityConfiguration, CancellationToken cancellationToken = default ) { diff --git a/CloudFabric.EAV.Service/IEAVService.cs b/CloudFabric.EAV.Service/IEAVService.cs deleted file mode 100644 index b82fa7f..0000000 --- a/CloudFabric.EAV.Service/IEAVService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace CloudFabric.EAV.Service; - -public interface IEAVService -{ -} diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs index b99c277..cd69a21 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs @@ -8,64 +8,16 @@ namespace CloudFabric.EAV.Service.Serialization; -/// -/// Entities are stored as c# dictionaries in projections - something similar to json. -/// That is required to not overload search engines with additional complexity of entity instances and attributes -/// allowing us to simply store -/// photo.likes = 4 instead of photo.attributes.where(a => a.machineName == "likes").value = 4 -/// -/// That comes with a price though - we now have to decode json-like dictionary back to entity instance view model. -/// Also it becomes not clear where is a serialization part and where is a deserializer. -/// -/// The following structure seems logical, not very understandable from the first sight however: -/// -/// -/// Serialization happens in -/// Projection builder creates dictionaries from EntityInstances and is responsible for storing projections data in -/// the best way suitable for search engines like elasticsearch. -/// -/// The segregation of reads and writes moves our decoding code out of ProjectionBuilder -/// and even out of CloudFabric.EAV.Domain because our ViewModels are on another layer - same layer as a service. -/// That means it's a service concern to decode dictionary into a ViewModel. -/// -/// -/// -public class EntityInstanceFromDictionaryDeserializer +public abstract class InstanceFromDictionaryDeserializer where T: EntityInstanceViewModel { - private readonly IMapper _mapper; + internal IMapper _mapper { get; set; } - public EntityInstanceFromDictionaryDeserializer(IMapper mapper) - { - _mapper = mapper; - } - - public EntityInstanceViewModel Deserialize( + public abstract T Deserialize( List attributesConfigurations, Dictionary record - ) - { - var entityInstance = new EntityInstanceViewModel - { - Id = (Guid)record["Id"]!, - TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null - ? (Guid)record["TenantId"]! - : null, - EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, - PartitionKey = (string)record["PartitionKey"]!, - Attributes = attributesConfigurations - .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) - .Select(attributeConfig => - DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) - ) - .ToList(), - CategoryPaths = record.ContainsKey("CategoryPaths") - ? ParseCategoryPaths(record["CategoryPaths"]) - : new List() - }; - return entityInstance; - } + ); - private List ParseCategoryPaths(object? paths) + internal List ParseCategoryPaths(object? paths) { var categoryPaths = new List(); if (paths is List pathsList) @@ -103,7 +55,7 @@ private List ParseCategoryPaths(object? paths) return categoryPaths; } - private AttributeInstanceViewModel DeserializeAttribute( + internal AttributeInstanceViewModel DeserializeAttribute( AttributeConfiguration attributeConfiguration, object? attributeValue ) @@ -145,7 +97,7 @@ private AttributeInstanceViewModel DeserializeAttribute( return attributeInstance; } - private AttributeInstanceViewModel DeserializeAttribute( + internal AttributeInstanceViewModel DeserializeAttribute( string attributeMachineName, EavAttributeType attributeType, object? attributeValue @@ -214,3 +166,96 @@ private AttributeInstanceViewModel DeserializeAttribute( return attributeInstance; } } + +/// +/// Entities are stored as c# dictionaries in projections - something similar to json. +/// That is required to not overload search engines with additional complexity of entity instances and attributes +/// allowing us to simply store +/// photo.likes = 4 instead of photo.attributes.where(a => a.machineName == "likes").value = 4 +/// +/// That comes with a price though - we now have to decode json-like dictionary back to entity instance view model. +/// Also it becomes not clear where is a serialization part and where is a deserializer. +/// +/// The following structure seems logical, not very understandable from the first sight however: +/// +/// +/// Serialization happens in +/// Projection builder creates dictionaries from EntityInstances and is responsible for storing projections data in +/// the best way suitable for search engines like elasticsearch. +/// +/// The segregation of reads and writes moves our decoding code out of ProjectionBuilder +/// and even out of CloudFabric.EAV.Domain because our ViewModels are on another layer - same layer as a service. +/// That means it's a service concern to decode dictionary into a ViewModel. +/// +/// +/// +public class EntityInstanceFromDictionaryDeserializer: InstanceFromDictionaryDeserializer +{ + + public EntityInstanceFromDictionaryDeserializer(IMapper mapper) + { + _mapper = mapper; + } + + public override EntityInstanceViewModel Deserialize( + List attributesConfigurations, + Dictionary record + ) + { + var entityInstance = new EntityInstanceViewModel + { + Id = (Guid)record["Id"]!, + TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null + ? (Guid)record["TenantId"]! + : null, + EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, + PartitionKey = (string)record["PartitionKey"]!, + Attributes = attributesConfigurations + .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) + .Select(attributeConfig => + DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) + ) + .ToList(), + CategoryPaths = record.ContainsKey("CategoryPaths") + ? ParseCategoryPaths(record["CategoryPaths"]) + : new List() + }; + return entityInstance; + } + +} + +public class CategoryFromDictionaryDeserializer : InstanceFromDictionaryDeserializer +{ + + public CategoryFromDictionaryDeserializer(IMapper mapper) + { + _mapper = mapper; + } + + public override CategoryViewModel Deserialize( + List attributesConfigurations, + Dictionary record + ) + { + var category = new CategoryViewModel() + { + Id = (Guid)record["Id"]!, + TenantId = record.ContainsKey("TenantId") && record["TenantId"] != null + ? (Guid)record["TenantId"]! + : null, + EntityConfigurationId = (Guid)record["EntityConfigurationId"]!, + PartitionKey = (string)record["PartitionKey"]!, + Attributes = attributesConfigurations + .Where(attributeConfig => record.ContainsKey(attributeConfig.MachineName)) + .Select(attributeConfig => + DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) + ) + .ToList(), + CategoryPaths = record.ContainsKey("CategoryPaths") + ? ParseCategoryPaths(record["CategoryPaths"]) + : new List() + }; + return category; + } +} From fc77c1e43c33a64f94cdbada44ba28cc6c659fa9 Mon Sep 17 00:00:00 2001 From: Dmytro Honcharenko Date: Mon, 31 Jul 2023 19:40:23 +0300 Subject: [PATCH 4/9] {{WIP}} --- CloudFabric.EAV.Service/EAVService.cs | 112 +++---- .../BaseQueryTests/BaseQueryTests.cs | 48 ++- .../CategoryTests/CategoryTests.cs | 54 ++-- .../EntityInstanceQueryingTests.cs | 22 +- .../JsonSerializationTests.cs | 40 +-- CloudFabric.EAV.Tests/Tests.cs | 306 +++++++++--------- 6 files changed, 303 insertions(+), 279 deletions(-) diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 4a0178c..d7f05e4 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; @@ -29,12 +30,11 @@ namespace CloudFabric.EAV.Service; -public class EAVService where V: EntityInstanceViewModel - where U: EntityInstanceUpdateRequest - where T: EntityInstanceBase +[SuppressMessage("ReSharper", "InconsistentNaming")] +public abstract class EAVService where TViewModel: EntityInstanceViewModel + where TUpdateRequest: EntityInstanceUpdateRequest + where TEntityType: EntityInstanceBase { - internal readonly AggregateRepositoryFactory _aggregateRepositoryFactory; - private readonly IProjectionRepository _attributeConfigurationProjectionRepository; @@ -45,12 +45,12 @@ private readonly IProjectionRepository internal readonly AggregateRepository _entityConfigurationRepository; - private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; + private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer _entityInstanceCreateUpdateRequestFromJsonDeserializer; - internal readonly AggregateRepository _entityInstanceRepository; - private readonly ILogger> _logger; + internal readonly AggregateRepository _entityInstanceRepository; + private readonly ILogger> _logger; internal readonly IMapper _mapper; private readonly JsonSerializerOptions _jsonSerializerOptions; internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory; @@ -60,9 +60,9 @@ internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer internal readonly AggregateRepository _categoryTreeRepository; internal readonly AggregateRepository _categoryInstanceRepository; - public EAVService( - ILogger> logger, - InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer, + protected EAVService( + ILogger> logger, + InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer, IMapper mapper, JsonSerializerOptions jsonSerializerOptions, AggregateRepositoryFactory aggregateRepositoryFactory, @@ -74,19 +74,18 @@ EventUserInfo userInfo _mapper = mapper; _jsonSerializerOptions = jsonSerializerOptions; - _aggregateRepositoryFactory = aggregateRepositoryFactory; _projectionRepositoryFactory = projectionRepositoryFactory; _userInfo = userInfo; - _attributeConfigurationRepository = _aggregateRepositoryFactory + _attributeConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityConfigurationRepository = _aggregateRepositoryFactory + _entityConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityInstanceRepository = _aggregateRepositoryFactory - .GetAggregateRepository(); + _entityInstanceRepository = aggregateRepositoryFactory + .GetAggregateRepository(); _attributeConfigurationProjectionRepository = _projectionRepositoryFactory @@ -101,9 +100,9 @@ EventUserInfo userInfo _attributeConfigurationRepository, jsonSerializerOptions ); - _categoryInstanceRepository = _aggregateRepositoryFactory + _categoryInstanceRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _categoryTreeRepository = _aggregateRepositoryFactory + _categoryTreeRepository = aggregateRepositoryFactory .GetAggregateRepository(); } @@ -158,7 +157,7 @@ private async Task CheckAttributesListMachineNameUnique( .Select(x => ((EntityAttributeConfigurationCreateUpdateReferenceRequest)x).AttributeConfigurationId) .ToList(); - List machineNames = new(); + List machineNames = new List(); if (referenceAttributes.Any()) { machineNames = (await GetAttributesByIds(referenceAttributes, cancellationToken)) @@ -192,10 +191,9 @@ private async Task attributesIds, CancellationToken cancellationToken) { // create attributes filter - Filter attributeIdFilter = new(nameof(AttributeConfigurationProjectionDocument.Id), + Filter attributeIdFilter = new Filter(nameof(AttributeConfigurationProjectionDocument.Id), FilterOperator.Equal, - attributesIds[0] - ); + attributesIds[0]); foreach (Guid attributesId in attributesIds.Skip(1)) { @@ -225,7 +223,7 @@ private async Task x.TreeId == treeId); @@ -423,7 +421,7 @@ CancellationToken cancellationToken nameof(entityConfigurationCreateRequest.Attributes), "Attributes machine name must be unique" ) - )!; + ); } foreach (EntityAttributeConfigurationCreateUpdateRequest attribute in entityConfigurationCreateRequest @@ -447,7 +445,7 @@ CancellationToken cancellationToken nameof(entityConfigurationCreateRequest.Attributes), "One or more attribute not found" ) - )!; + ); } } @@ -515,7 +513,7 @@ await _entityConfigurationRepository.SaveAsync( cancellationToken ).ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + return (_mapper.Map(entityConfiguration), null); } public async Task<(EntityConfigurationViewModel?, ProblemDetails?)> UpdateEntityConfiguration( @@ -539,7 +537,7 @@ await _entityConfigurationRepository.SaveAsync( new ValidationErrorResponse(nameof(entityUpdateRequest.Attributes), "Attributes machine name must be unique" ) - )!; + ); } EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( @@ -551,7 +549,7 @@ await _entityConfigurationRepository.SaveAsync( if (entityConfiguration == null) { return (null, - new ValidationErrorResponse(nameof(entityUpdateRequest.Id), "Entity configuration not found"))!; + new ValidationErrorResponse(nameof(entityUpdateRequest.Id), "Entity configuration not found")); } var entityConfigurationExistingAttributes = @@ -585,7 +583,10 @@ await _entityConfigurationRepository.SaveAsync( cancellationToken ).ConfigureAwait(false); - if (attributeConfiguration != null && !attributeConfiguration.IsDeleted) + if (attributeConfiguration is + { + IsDeleted: false + }) { if (attributeShouldBeAdded) { @@ -664,7 +665,7 @@ await CreateAttribute( await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + return (_mapper.Map(entityConfiguration), null); } public async Task<(EntityConfigurationViewModel?, ProblemDetails?)> AddAttributeToEntityConfiguration( @@ -693,7 +694,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c if (entityConfiguration.Attributes.Any(x => x.AttributeConfigurationId == attributeConfiguration.Id)) { - return (null, new ValidationErrorResponse(nameof(attributeId), "Attribute has already been added"))!; + return (null, new ValidationErrorResponse(nameof(attributeId), "Attribute has already been added")); } if (!await IsAttributeMachineNameUniqueForEntityConfiguration( @@ -713,7 +714,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); - return (_mapper.Map(entityConfiguration), null)!; + return (_mapper.Map(entityConfiguration), null); } public async Task<(AttributeConfigurationViewModel, ProblemDetails)> CreateAttributeAndAddToEntityConfiguration( @@ -796,7 +797,10 @@ public async Task DeleteAttributes(List attributesIds, CancellationToken c cancellationToken ); - if (attributeConfiguration != null && !attributeConfiguration.IsDeleted) + if (attributeConfiguration is + { + IsDeleted: false + }) { attributeConfiguration.Delete(); @@ -812,11 +816,7 @@ await _attributeConfigurationRepository ) ); - Filter filters = new( - filterPropertyName, - FilterOperator.Equal, - attributesIds[0] - ); + Filter filters = new Filter(filterPropertyName, FilterOperator.Equal, attributesIds[0]); foreach (Guid attributeId in attributesIds.Skip(1)) { @@ -940,18 +940,18 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR // return _mapper.Map>(records); // } - #region EntityInstance + #region BaseEntityInstance - public async Task GetEntityInstance(Guid id, string partitionKey) + public async Task GetEntityInstance(Guid id, string partitionKey) { - T? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); + TEntityType? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); - return _mapper.Map(entityInstance); + return _mapper.Map(entityInstance); } public async Task GetEntityInstanceJsonMultiLanguage(Guid id, string partitionKey) { - V? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); return SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); } @@ -962,12 +962,12 @@ public async Task GetEntityInstanceJsonSingleLanguage( string language, string fallbackLanguage = "en-US") { - V? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); return SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); } - public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(V? entityInstanceViewModel) + protected JsonDocument SerializeEntityInstanceToJsonMultiLanguage(TViewModel? entityInstanceViewModel) { var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); @@ -976,8 +976,8 @@ public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(V? entityInstance return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); } - public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( - V? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" + private JsonDocument SerializeEntityInstanceToJsonSingleLanguage( + TViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" ) { var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); @@ -987,10 +987,10 @@ public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); } - public async Task<(V, ProblemDetails)> UpdateEntityInstance(string partitionKey, - U updateRequest, CancellationToken cancellationToken) + public async Task<(TViewModel, ProblemDetails)> UpdateEntityInstance(string partitionKey, + TUpdateRequest updateRequest, CancellationToken cancellationToken) { - T? entityInstance = + TEntityType? entityInstance = await _entityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); if (entityInstance == null) @@ -1092,7 +1092,7 @@ await GetAttributeConfigurationsForEntityConfiguration( //TODO: Throw a error when ready } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } /// @@ -1104,7 +1104,7 @@ await GetAttributeConfigurationsForEntityConfiguration( /// /// /// - public async Task> QueryInstances( + public async Task> QueryInstances( Guid entityConfigurationId, ProjectionQuery query, CancellationToken cancellationToken = default @@ -1229,10 +1229,10 @@ public async Task> QueryInstancesJsonSingleL ); } - public async Task<(V, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, + public async Task<(TViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, string entityInstancePartitionKey, Guid treeId, Guid? newParentId, CancellationToken cancellationToken = default) { - T? entityInstance = await _entityInstanceRepository + TEntityType? entityInstance = await _entityInstanceRepository .LoadAsync(entityInstanceId, entityInstancePartitionKey, cancellationToken).ConfigureAwait(false); if (entityInstance == null) { @@ -1256,7 +1256,7 @@ public async Task> QueryInstancesJsonSingleL throw new Exception("Entity was not saved"); } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } internal async Task> GetAttributeConfigurationsForEntityConfiguration( diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index 3ffcbda..74cbde1 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -18,9 +18,12 @@ namespace CloudFabric.EAV.Tests.BaseQueryTests; public abstract class BaseQueryTests { - protected EAVService _eavService; + protected EAVEntityInstanceService _eavEntityInstanceService; + protected EAVCategoryService _eavCategoryService; + protected IEventStore _eventStore; - protected ILogger _logger; + protected ILogger _eiLogger; + protected ILogger _cLogger; protected virtual TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(0); @@ -32,15 +35,23 @@ public abstract class BaseQueryTests public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _logger = loggerFactory.CreateLogger(); - - var configuration = new MapperConfiguration(cfg => + _eiLogger = loggerFactory.CreateLogger(); + _cLogger = loggerFactory.CreateLogger(); + + var eiConfiguration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); } ); - IMapper? mapper = configuration.CreateMapper(); - + + var cConfiguration = new MapperConfiguration(cfg => + { + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVCategoryService))); + } + ); + IMapper? eiMapper = eiConfiguration.CreateMapper(); + IMapper? cMapper = eiConfiguration.CreateMapper(); + _eventStore = GetEventStore(); await _eventStore.Initialize(); @@ -67,10 +78,23 @@ public async Task SetUp() await projectionsEngine.StartAsync("TestInstance"); - _eavService = new EAVService( - _logger, - mapper, - new JsonSerializerOptions() + _eavEntityInstanceService = new EAVEntityInstanceService( + _eiLogger, + eiMapper, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + }, + aggregateRepositoryFactory, + projectionRepositoryFactory, + new EventUserInfo(Guid.NewGuid()) + ); + + _eavCategoryService = new EAVCategoryService( + _cLogger, + cMapper, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index 64431ee..205584e 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -32,7 +32,7 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests // Create config for categories EntityConfigurationCreateRequest categoryConfigurationCreateRequest = EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(0, 9); - (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( categoryConfigurationCreateRequest, CancellationToken.None ); @@ -44,13 +44,13 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests EntityConfigurationId = categoryConfiguration!.Id }; - (HierarchyViewModel createdTree, _) = await _eavService.CreateCategoryTreeAsync(treeRequest, + (HierarchyViewModel createdTree, _) = await _eavCategoryService.CreateCategoryTreeAsync(treeRequest, categoryConfigurationCreateRequest.TenantId, CancellationToken.None ); (CategoryViewModel laptopsCategory, _) = - await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, null, categoryConfigurationCreateRequest.TenantId, @@ -60,7 +60,7 @@ await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryIns )); (CategoryViewModel gamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, laptopsCategory.Id, categoryConfigurationCreateRequest.TenantId, @@ -70,7 +70,7 @@ await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryIns )); (CategoryViewModel officeLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, laptopsCategory.Id, categoryConfigurationCreateRequest.TenantId, @@ -80,7 +80,7 @@ await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryIns )); (CategoryViewModel asusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, gamingLaptopsCategory.Id, categoryConfigurationCreateRequest.TenantId, @@ -90,7 +90,7 @@ await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryIns )); (CategoryViewModel rogAsusGamingLaptopsCategory, _) = - await _eavService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, + await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, asusGamingLaptopsCategory.Id, categoryConfigurationCreateRequest.TenantId, @@ -124,7 +124,7 @@ public async Task GetTreeViewAsync() CategoryViewModel rogAsusGamingLaptopsCategory) = await BuildTestTreeAsync(); List list = - await _eavService.GetCategoryTreeViewAsync(createdTree.Id); + await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id); EntityTreeInstanceViewModel? laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); laptops.Should().NotBeNull(); @@ -142,11 +142,11 @@ public async Task GetTreeViewAsync() asusGamingLaptops?.Children.FirstOrDefault(x => x.Id == rogAsusGamingLaptopsCategory.Id); rogAsusGamingLaptops.Should().NotBeNull(); - list = await _eavService.GetCategoryTreeViewAsync(createdTree.Id, laptopsCategory.Id); + list = await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, laptopsCategory.Id); laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); laptops.Children.Count.Should().Be(0); - list = await _eavService.GetCategoryTreeViewAsync(createdTree.Id, asusGamingLaptopsCategory.Id); + list = await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, asusGamingLaptopsCategory.Id); laptops = list.FirstOrDefault(x => x.Id == laptopsCategory.Id); gamingLaptops = laptops.Children.FirstOrDefault(x => x.Id == gamingLaptopsCategory.Id); @@ -165,7 +165,7 @@ public async Task GetTreeViewAsync_CategoryNofFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetCategoryTreeViewAsync(createdTree.Id, Guid.NewGuid()); + Func action = async () => await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, Guid.NewGuid()); await action.Should().ThrowAsync(); } @@ -178,7 +178,7 @@ public async Task GetSubcategoriesBranch_Success() await Task.Delay(ProjectionsUpdateDelay); var categoryPathValue = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}"; - ProjectionQueryResult subcategories12 = await _eavService.QueryInstances( + ProjectionQueryResult subcategories12 = await _eavEntityInstanceService.QueryInstances( createdTree.EntityConfigurationId, new ProjectionQuery { @@ -200,19 +200,19 @@ public async Task GetSubcategories_Success() CategoryViewModel gamingLaptopsCategory, CategoryViewModel officeLaptopsCategory, CategoryViewModel asusGamingLaptops, CategoryViewModel _) = await BuildTestTreeAsync(); - var subcategories = await _eavService.GetSubcategories(createdTree.Id, null); + var subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, null); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, laptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, laptopsCategory.Id); subcategories.Count.Should().Be(2); - subcategories = await _eavService.GetSubcategories(createdTree.Id, gamingLaptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, gamingLaptopsCategory.Id); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, asusGamingLaptops.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, asusGamingLaptops.Id); subcategories.Count.Should().Be(1); - subcategories = await _eavService.GetSubcategories(createdTree.Id, officeLaptopsCategory.Id); + subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, officeLaptopsCategory.Id); subcategories.Count.Should().Be(0); } @@ -223,7 +223,7 @@ public async Task GetSubcategories_TreeNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - Func action = async () => await _eavService.GetSubcategories(Guid.NewGuid()); + Func action = async () => await _eavCategoryService.GetSubcategories(Guid.NewGuid()); await action.Should().ThrowAsync().WithMessage("Category tree not found"); } @@ -235,7 +235,7 @@ public async Task GetSubcategories_ParentNotFound() CategoryViewModel _, CategoryViewModel _, CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); - var result = await _eavService.GetSubcategories(createdTree.Id, parentId: Guid.NewGuid()); + var result = await _eavCategoryService.GetSubcategories(createdTree.Id, parentId: Guid.NewGuid()); result.Should().BeEmpty(); } @@ -248,7 +248,7 @@ public async Task MoveAndGetItemsFromCategory_Success() EntityConfigurationCreateRequest itemEntityConfig = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( itemEntityConfig, CancellationToken.None ); @@ -256,11 +256,11 @@ public async Task MoveAndGetItemsFromCategory_Success() EntityInstanceCreateRequest itemInstanceRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(itemEntityConfiguration.Id); - var (_, _) = await _eavService.CreateEntityInstance(itemInstanceRequest); + var (_, _) = await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); (EntityInstanceViewModel createdItemInstance2, _) = - await _eavService.CreateEntityInstance(itemInstanceRequest); - (createdItemInstance2, _) = await _eavService.UpdateCategoryPath(createdItemInstance2.Id, + await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); + (createdItemInstance2, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance2.Id, createdItemInstance2.PartitionKey, createdTree.Id, asusGamingLaptops.Id, @@ -268,8 +268,8 @@ public async Task MoveAndGetItemsFromCategory_Success() ); (EntityInstanceViewModel createdItemInstance3, _) = - await _eavService.CreateEntityInstance(itemInstanceRequest); - (_, _) = await _eavService.UpdateCategoryPath(createdItemInstance3.Id, + await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); + (_, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance3.Id, createdItemInstance2.PartitionKey, createdTree.Id, rogAsusGamingLaptops.Id, @@ -281,7 +281,7 @@ public async Task MoveAndGetItemsFromCategory_Success() var pathFilterValue121 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom121 = await _eavService.QueryInstances( + ProjectionQueryResult itemsFrom121 = await _eavEntityInstanceService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { @@ -295,7 +295,7 @@ public async Task MoveAndGetItemsFromCategory_Success() var pathFilterValue1211 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}/{_rogAsusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom1211 = await _eavService.QueryInstances( + ProjectionQueryResult itemsFrom1211 = await _eavEntityInstanceService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs index 65578d5..2128c25 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs @@ -28,12 +28,12 @@ public async Task TestCreateInstanceAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -43,7 +43,7 @@ public async Task TestCreateInstanceAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -58,7 +58,7 @@ public async Task TestCreateInstanceAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -72,12 +72,12 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -87,7 +87,7 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -102,7 +102,7 @@ public async Task TestCreateInstanceUpdateAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -119,7 +119,7 @@ public async Task TestCreateInstanceUpdateAndQuery() }; (EntityInstanceViewModel updateResult, ProblemDetails updateErrors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), new EntityInstanceUpdateRequest { Id = createdInstance.Id, @@ -133,7 +133,7 @@ await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? searchResultsAfterUpdate = await _eavService + ProjectionQueryResult? searchResultsAfterUpdate = await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); searchResultsAfterUpdate?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -150,7 +150,7 @@ await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), .String.Should() .Be("Азул 2"); - var resultsJson = await _eavService + var resultsJson = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); var resultString = JsonSerializer.Serialize(resultsJson); diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index 386d83a..f7ebd91 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -49,12 +49,12 @@ public async Task TestCreateInstanceMultiLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -64,7 +64,7 @@ public async Task TestCreateInstanceMultiLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonMultiLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance( + await _eavEntityInstanceService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -86,7 +86,7 @@ await _eavService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -123,7 +123,7 @@ await _eavService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -131,7 +131,7 @@ await _eavService.CreateEntityInstance( resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") .GetProperty("ru-RU").GetString().Should().Be("Азул"); - ProjectionQueryResult resultsJsonSingleLanguage = await _eavService + ProjectionQueryResult resultsJsonSingleLanguage = await _eavEntityInstanceService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "en-US" ); @@ -139,7 +139,7 @@ await _eavService.CreateEntityInstance( resultsJsonSingleLanguage.Records.First().Document.RootElement.GetProperty("name") .GetString().Should().Be("Azul"); - ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavService + ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavEntityInstanceService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "ru-RU" ); @@ -153,14 +153,14 @@ await _eavService.CreateEntityInstance( var firstDocumentId = resultsJsonMultiLanguage.Records[0]?.Document!.RootElement.GetProperty("id").GetString(); - var oneInstanceJsonMultiLang = await _eavService.GetEntityInstanceJsonMultiLanguage( + var oneInstanceJsonMultiLang = await _eavEntityInstanceService.GetEntityInstanceJsonMultiLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()! ); oneInstanceJsonMultiLang.RootElement.GetProperty("name").GetProperty("en-US").GetString().Should().Be("Azul"); - var oneInstanceJsonSingleLang = await _eavService.GetEntityInstanceJsonSingleLanguage( + var oneInstanceJsonSingleLang = await _eavEntityInstanceService.GetEntityInstanceJsonSingleLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()!, "en-US" @@ -175,12 +175,12 @@ public async Task TestCreateInstanceSingleLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -190,7 +190,7 @@ public async Task TestCreateInstanceSingleLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonSingleLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance( + await _eavEntityInstanceService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -212,7 +212,7 @@ await _eavService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavService + ProjectionQueryResult? results = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -249,7 +249,7 @@ await _eavService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -266,9 +266,9 @@ public async Task CreateCategoryInstance() EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(); (EntityConfigurationViewModel? createdCategoryConfiguration, _) = - await _eavService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); - (HierarchyViewModel hierarchy, _) = await _eavService.CreateCategoryTreeAsync( + (HierarchyViewModel hierarchy, _) = await _eavCategoryService.CreateCategoryTreeAsync( new CategoryTreeCreateRequest { EntityConfigurationId = createdCategoryConfiguration!.Id, @@ -277,7 +277,7 @@ public async Task CreateCategoryInstance() }, categoryConfigurationRequest.TenantId); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdCategoryConfiguration!.Id ); @@ -288,7 +288,7 @@ public async Task CreateCategoryInstance() createdCategoryConfiguration!.Id, hierarchy.Id, createdCategoryConfiguration.TenantId!.Value ); - (JsonDocument? createdCategory, _) = await _eavService.CreateCategoryInstance(categoryJsonStringCreateRequest); + (JsonDocument? createdCategory, _) = await _eavCategoryService.CreateCategoryInstance(categoryJsonStringCreateRequest); var query = new ProjectionQuery { @@ -298,7 +298,7 @@ public async Task CreateCategoryInstance() } }; - var results = await _eavService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); + var results = await _eavEntityInstanceService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); var resultDocument = results?.Records.Select(r => r.Document).First(); @@ -308,7 +308,7 @@ public async Task CreateCategoryInstance() JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), _serializerOptions)! .First().TreeId.Should().Be(hierarchy.Id); - (createdCategory, _) = await _eavService.CreateCategoryInstance( + (createdCategory, _) = await _eavCategoryService.CreateCategoryInstance( categoryJsonStringCreateRequest, "test-category", createdCategoryConfiguration.Id, diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index 120497d..5de0efd 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -39,10 +39,10 @@ public class Tests { private AggregateRepositoryFactory _aggregateRepositoryFactory; - private EAVService _eavService; + private EAVEntityInstanceService _eavEntityInstanceService; private IEventStore _eventStore; - private ILogger _logger; + private ILogger _eiLogger; private PostgresqlProjectionRepositoryFactory _projectionRepositoryFactory; private IMapper _mapper; @@ -50,11 +50,11 @@ public class Tests public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _logger = loggerFactory.CreateLogger(); + _eiLogger = loggerFactory.CreateLogger(); var configuration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); } ); _mapper = configuration.CreateMapper(); @@ -95,8 +95,8 @@ public async Task SetUp() await projectionsEngine.StartAsync("TestInstance"); - _eavService = new EAVService( - _logger, + _eavEntityInstanceService = new EAVEntityInstanceService( + _eiLogger, _mapper, new JsonSerializerOptions() { @@ -143,19 +143,19 @@ public async Task CreateInstance_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); validationErrors.Should().BeNull(); createdInstance.Id.Should().NotBeEmpty(); @@ -168,7 +168,7 @@ public async Task CreateInstance_InvalidConfigurationId() EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(Guid.NewGuid()); (EntityInstanceViewModel result, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); result.Should().BeNull(); validationErrors.Should().BeOfType(); validationErrors.As().Errors.Should().ContainKey("EntityConfigurationId"); @@ -182,20 +182,20 @@ public async Task CreateInstance_MissingRequiredAttribute() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); var requiredAttributeMachineName = "players_min"; EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes = entityInstanceCreateRequest.Attributes .Where(a => a.ConfigurationAttributeMachineName != requiredAttributeMachineName).ToList(); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); createdInstance.Should().BeNull(); validationErrors.As().Errors.Should().ContainKey(requiredAttributeMachineName); @@ -224,13 +224,13 @@ public async Task CreateInstance_MissingRequiredAttributeValue() } } }); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); EntityConfigurationViewModel configuration = - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes.Add( @@ -241,7 +241,7 @@ public async Task CreateInstance_MissingRequiredAttributeValue() }); (EntityInstanceViewModel createdInstance, ProblemDetails validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); createdInstance.Should().BeNull(); } @@ -266,12 +266,12 @@ public async Task CreateInstance_IgnoreRequiredCheck_Success() } } }); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - await _eavService.GetEntityConfiguration(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfiguration(createdConfiguration.Id); EntityInstanceCreateRequest entityInstanceCreateRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); entityInstanceCreateRequest.Attributes.Add( @@ -282,7 +282,7 @@ public async Task CreateInstance_IgnoreRequiredCheck_Success() }); (EntityInstanceViewModel? createdInstance, ProblemDetails? validationErrors) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest, requiredAttributesCanBeNull: true); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest, requiredAttributesCanBeNull: true); validationErrors.Should().BeNull(); createdInstance.Should().NotBeNull(); } @@ -293,7 +293,7 @@ public async Task CreateEntityConfiguration_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfiguration, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -311,7 +311,7 @@ await _eavService.CreateEntityConfiguration( createdConfiguration.Attributes.Count.Should().Be(configurationCreateRequest.Attributes.Count); var configurationWithAttributes = - await _eavService.GetEntityConfigurationWithAttributes(createdConfiguration.Id); + await _eavEntityInstanceService.GetEntityConfigurationWithAttributes(createdConfiguration.Id); configurationWithAttributes.Should().BeEquivalentTo(configurationCreateRequest); } @@ -323,7 +323,7 @@ public async Task CreateEntityConfiguration_ValidationError() EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); configurationCreateRequest.Name = new List(); (EntityConfigurationViewModel? createdConfiguration, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -354,7 +354,7 @@ public async Task CreateEntityConfiguration_AttributesMachineNamesAreNotUnique() ); (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -372,12 +372,12 @@ public async Task GetEntityConfiguration_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -417,7 +417,7 @@ public async Task UpdateAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); // update added attribute @@ -426,7 +426,7 @@ public async Task UpdateAttribute_Success() numberAttribute.MinimumValue = 0; numberAttribute.MaximumValue = 50; - (AttributeConfigurationViewModel? _, ProblemDetails? error) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? _, ProblemDetails? error) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, numberAttribute, CancellationToken.None @@ -434,7 +434,7 @@ public async Task UpdateAttribute_Success() error.Should().BeNull(); - AttributeConfigurationViewModel updatedAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel updatedAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -480,14 +480,14 @@ public async Task UpdateAttribute_ValidationError() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); // update added attribute numberAttribute.Name = new List(); (AttributeConfigurationViewModel? updatedResult, ProblemDetails? errors) = - await _eavService.UpdateAttribute( + await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, numberAttribute, CancellationToken.None @@ -506,21 +506,21 @@ public async Task DeleteAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel entityConfig, ProblemDetails? _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); Guid attributeToDelete = entityConfig.Attributes.Select(x => x.AttributeConfigurationId).FirstOrDefault(); - await _eavService.DeleteAttributes(new List { attributeToDelete }, CancellationToken.None); + await _eavEntityInstanceService.DeleteAttributes(new List { attributeToDelete }, CancellationToken.None); EntityConfigurationViewModel entityConfAfterAttributeDeleted = - await _eavService.GetEntityConfiguration(entityConfig.Id); + await _eavEntityInstanceService.GetEntityConfiguration(entityConfig.Id); entityConfAfterAttributeDeleted.Attributes.Count().Should().Be(entityConfig.Attributes.Count() - 1); - Func act = async () => await _eavService.GetAttribute(attributeToDelete); + Func act = async () => await _eavEntityInstanceService.GetAttribute(attributeToDelete); await act.Should().ThrowAsync(); ProjectionQueryResult attributesProjections = - await _eavService.ListAttributes(new ProjectionQuery + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Filters = new List { @@ -540,7 +540,7 @@ await _eavService.ListAttributes(new ProjectionQuery public async Task DeleteEntityAttributeFromEntity_EntityNotFound() { Func act = async () => - await _eavService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, + await _eavEntityInstanceService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, Guid.NewGuid(), CancellationToken.None ); @@ -553,13 +553,13 @@ public async Task DeleteEntityAttributeFromEntity_DeleteNotExistingAttribute() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel entityConfig, ProblemDetails? _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, + await _eavEntityInstanceService.DeleteAttributesFromEntityConfiguration(new List { Guid.NewGuid() }, entityConfig.Id, CancellationToken.None ); - EntityConfigurationViewModel entityConfigAfterDeletingNotExistingAttribute = await _eavService.GetEntityConfiguration(entityConfig.Id); + EntityConfigurationViewModel entityConfigAfterDeletingNotExistingAttribute = await _eavEntityInstanceService.GetEntityConfiguration(entityConfig.Id); entityConfigAfterDeletingNotExistingAttribute.Attributes.Count.Should().Be(entityConfig.Attributes.Count); } @@ -583,7 +583,7 @@ public async Task GetAttributeByUsedEntities_Success() MaximumValue = 100, MinimumValue = -100 }; - (AttributeConfigurationViewModel numberAttribute, _) = await _eavService.CreateAttribute(numberAttributeRequest); + (AttributeConfigurationViewModel numberAttribute, _) = await _eavEntityInstanceService.CreateAttribute(numberAttributeRequest); var textAttributeRequest = new TextAttributeConfigurationCreateUpdateRequest { @@ -597,7 +597,7 @@ public async Task GetAttributeByUsedEntities_Success() MaxLength = 100, DefaultValue = "-" }; - (AttributeConfigurationViewModel textAttribute, _) = await _eavService.CreateAttribute(textAttributeRequest); + (AttributeConfigurationViewModel textAttribute, _) = await _eavEntityInstanceService.CreateAttribute(textAttributeRequest); // Create entity and add attributes var configurationCreateRequest = new EntityConfigurationCreateRequest @@ -609,10 +609,10 @@ public async Task GetAttributeByUsedEntities_Success() } }; (EntityConfigurationViewModel? createdFirstEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdFirstEntity.Id); - await _eavService.AddAttributeToEntityConfiguration(textAttribute.Id, createdFirstEntity.Id); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdFirstEntity.Id); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(textAttribute.Id, createdFirstEntity.Id); // Get attributes by UsedByEntityConfigurationIds ProjectionQuery query = new ProjectionQuery() @@ -644,8 +644,8 @@ public async Task GetAttributeByUsedEntities_Success() } }; (EntityConfigurationViewModel? createdSecondEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); - await _eavService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdSecondEntity.Id); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.AddAttributeToEntityConfiguration(numberAttribute.Id, createdSecondEntity.Id); // Get attribute by one of UsedByEntityConfigurationIds query.Filters = new() @@ -663,7 +663,7 @@ public async Task GetAttributeByUsedEntities_Success() result.Records.FirstOrDefault().Document.UsedByEntityConfigurationIds.Count.Should().Be(2); // Get after delete - await _eavService.DeleteAttributes(new List() { numberAttribute.Id }); + await _eavEntityInstanceService.DeleteAttributes(new List() { numberAttribute.Id }); result = await projectionRepository.Query(query); result.TotalRecordsFound.Should().Be(0); @@ -695,7 +695,7 @@ public async Task GetAttributeByUsedEntities_Success() Attributes = createAttrList }; (EntityConfigurationViewModel? createdThirdEntity, _) = - await _eavService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configurationCreateRequest, CancellationToken.None); query.Filters = new() { @@ -781,7 +781,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_MachineNamesAreNot (configRequest.Attributes[0] as AttributeConfigurationCreateUpdateRequest)!.MachineName!; (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest { @@ -806,7 +806,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_MachineNamesAreNot }; (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); entityConfig.Should().BeNull(); error.Should().NotBeNull(); error.Should().BeOfType(); @@ -822,7 +822,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_Success() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); const string newAttributeMachineName = "avg_time_mins"; var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest @@ -847,7 +847,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_Success() Name = configRequest.Name }; - _ = await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + _ = await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); //var newAttrIndex = updatedConfig.Attributes.FindIndex(a => a.MachineName == newAttributeMachineName); //newAttrIndex.Should().BePositive(); //var newAttribute = updatedConfig.Attributes[newAttrIndex]; @@ -863,7 +863,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_ValidationError() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); const string newAttributeMachineName = "some_new_attribute_name"; var newAttributeRequest = new NumberAttributeConfigurationCreateUpdateRequest @@ -891,7 +891,7 @@ public async Task UpdateEntityConfiguration_AddedNewAttribute_ValidationError() }; (_, ProblemDetails? errors) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); errors.Should().NotBeNull(); errors.As().Errors.Should().ContainKey(newAttributeMachineName); errors.As().Errors[newAttributeMachineName].Should() @@ -906,7 +906,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() EntityConfigurationCreateRequest configRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configRequest, CancellationToken.None); var newNameRequest = new List { new() { CultureInfoId = cultureId, String = newName } @@ -919,7 +919,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() Name = configRequest.Name }; (EntityConfigurationViewModel? updatedConfig, ProblemDetails? updateError) = - await _eavService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); + await _eavEntityInstanceService.UpdateEntityConfiguration(updateRequest, CancellationToken.None); updateError.Should().NotBeNull(); updateError.As().Errors.First().Key.Should().Be("Attributes[0]"); @@ -930,7 +930,7 @@ public async Task UpdateEntityConfiguration_ChangeAttributeName_Fail() public async Task EntityConfigurationProjectionCreated() { ProjectionQueryResult configurationItemsStart = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.MachineName == "BoardGame" ), @@ -943,14 +943,14 @@ await _eavService.ListEntityConfigurations( EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); (EntityConfigurationViewModel?, ProblemDetails?) createdConfiguration = - await _eavService.CreateEntityConfiguration( + await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); // verify projection is created ProjectionQueryResult configurationItems = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.MachineName == "BoardGame" ) @@ -967,19 +967,19 @@ public async Task GetEntityConfigurationProjectionByTenantId_Success() EntityConfigurationCreateRequest configurationCreateRequest2 = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration1, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration1, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest1, CancellationToken.None ); - (EntityConfigurationViewModel? createdConfiguration2, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration2, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest2, CancellationToken.None ); // verify projection is created ProjectionQueryResult configurationItems = - await _eavService.ListEntityConfigurations( + await _eavEntityInstanceService.ListEntityConfigurations( ProjectionQueryExpressionExtensions.Where(x => x.TenantId == createdConfiguration2.TenantId ) @@ -1016,11 +1016,11 @@ public async Task CreateTextAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.MachineName.Should().Be(textAttrbiuteRequest.MachineName); @@ -1057,14 +1057,14 @@ public async Task CreateTextAttribute_MaxLengthValidationError() }; // check - (_, ProblemDetails? errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, ProblemDetails? errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors.Count.Should().Be(1); errors.As().Errors.First().Value.First().Should().Be("Max length can't be negative or zero"); // check for negative max length textAttrbiuteRequest.MaxLength = -10; - (_, errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors.First().Value.First().Should().NotBeNullOrEmpty(); } @@ -1094,7 +1094,7 @@ public async Task CreateTextAttribute_DefaultValueValidationError() Attributes = new List { textAttrbiuteRequest } }; - (_, ProblemDetails? errors) = await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + (_, ProblemDetails? errors) = await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); errors.As().Errors .First().Value.First().Should().Be("Default value length cannot be greater than MaxLength"); @@ -1129,11 +1129,11 @@ public async Task CreateNumberAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(numberAttribute.Name); @@ -1145,7 +1145,7 @@ public async Task CreateNumberAttribute_ValidationError() var request = new NumberAttributeConfigurationCreateUpdateRequest { MachineName = "avg_time_mins" }; (AttributeConfigurationViewModel? result, ValidationErrorResponse? errors) = - await _eavService.CreateAttribute(request); + await _eavEntityInstanceService.CreateAttribute(request); result.Should().BeNull(); errors.Should().NotBeNull(); errors.As().Errors.Should().ContainKey(request.MachineName); @@ -1183,11 +1183,11 @@ public async Task CreateMoneyAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(moneyAttribute.Name); @@ -1233,11 +1233,11 @@ public async Task CreateMoneyAttributeCustomCurrency_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(moneyAttribute.Name); @@ -1274,7 +1274,7 @@ public async Task CreateMoneyAttribute_InvalidDefaultId() }; (EntityConfigurationViewModel? created, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Should().BeNull(); errors.Should().NotBeNull(); } @@ -1311,7 +1311,7 @@ public async Task CreateMoneyAttribute_EmptyList() }; (EntityConfigurationViewModel? created, ProblemDetails? errors) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Should().BeNull(); errors.Should().NotBeNull(); } @@ -1347,10 +1347,10 @@ public async Task CreateFileAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); - AttributeConfigurationViewModel createdAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -1386,19 +1386,19 @@ public async Task UpdateFileAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); fileAttribute.IsDownloadable = false; fileAttribute.IsRequired = false; - (AttributeConfigurationViewModel? updated, _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updated, _) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes[0].AttributeConfigurationId, fileAttribute, CancellationToken.None ); - AttributeConfigurationViewModel createdAttribute = await _eavService + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService .GetAttribute(updated!.Id, CancellationToken.None); createdAttribute.IsRequired.Should().Be(fileAttribute.IsRequired); @@ -1432,7 +1432,7 @@ public async Task CreateFileAttributeInstance_Success() }; (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); createdConfig!.Attributes.Count.Should().Be(1); var instanceRequest = new EntityInstanceCreateRequest @@ -1453,11 +1453,11 @@ public async Task CreateFileAttributeInstance_Success() }; (EntityInstanceViewModel createdInstance, ProblemDetails _) = - await _eavService.CreateEntityInstance(instanceRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceRequest); createdInstance.Should().NotBeNull(); - createdInstance = await _eavService.GetEntityInstance(createdInstance.Id, createdConfig.Id.ToString()); + createdInstance = await _eavEntityInstanceService.GetEntityInstance(createdInstance.Id, createdConfig.Id.ToString()); createdInstance.Attributes.Count.Should().Be(1); createdInstance.Attributes[0] .As() @@ -1500,10 +1500,10 @@ public async Task GetNumberAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); created.Attributes.Count.Should().Be(1); - AttributeConfigurationViewModel createdAttribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel createdAttribute = await _eavEntityInstanceService.GetAttribute( created.Attributes[0].AttributeConfigurationId ); @@ -1549,11 +1549,11 @@ public async Task CreateValueFromListAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(valueFromListAttribute.Name); @@ -1601,7 +1601,7 @@ public async Task CreateValueFromListAttribute_ValidationError() // case check repeated name (EntityConfigurationViewModel entity, ProblemDetails errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1615,7 +1615,7 @@ public async Task CreateValueFromListAttribute_ValidationError() }; (entity, errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1626,7 +1626,7 @@ public async Task CreateValueFromListAttribute_ValidationError() valueFromListAttribute.ValuesList = new List(); (entity, errors) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); entity.Should().BeNull(); errors.Should().BeOfType(); @@ -1661,7 +1661,7 @@ public async Task UpdateValueFromListAttribute_Success() }; (AttributeConfigurationViewModel? valueFromListAttribute, _) = - await _eavService.CreateAttribute(valueFromListAttributeCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(valueFromListAttributeCreateRequest, CancellationToken.None); // create request with changed properties and update attribute @@ -1672,7 +1672,7 @@ public async Task UpdateValueFromListAttribute_Success() }; (AttributeConfigurationViewModel? changedAttribute, _) = - await _eavService.UpdateAttribute(valueFromListAttribute.Id, + await _eavEntityInstanceService.UpdateAttribute(valueFromListAttribute.Id, valueFromListAttributeCreateRequest!, CancellationToken.None ); @@ -1718,10 +1718,10 @@ public async Task CreateEntityInstanceWithValueFromListAttribute_ValidationError }; (EntityConfigurationViewModel? entityConfiguration, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create entity instance using wrong type of attribute - (EntityInstanceViewModel result, ProblemDetails validationErrors) = await _eavService.CreateEntityInstance( + (EntityInstanceViewModel result, ProblemDetails validationErrors) = await _eavEntityInstanceService.CreateEntityInstance( new EntityInstanceCreateRequest { EntityConfigurationId = entityConfiguration.Id, @@ -1740,7 +1740,7 @@ public async Task CreateEntityInstanceWithValueFromListAttribute_ValidationError validationErrors.As().Errors["testValueAttr"].First().Should() .Be("Cannot validate attribute. Expected attribute type: Value from list"); - (result, validationErrors) = await _eavService.CreateEntityInstance(new EntityInstanceCreateRequest + (result, validationErrors) = await _eavEntityInstanceService.CreateEntityInstance(new EntityInstanceCreateRequest { EntityConfigurationId = entityConfiguration.Id, Attributes = new List @@ -1794,11 +1794,11 @@ public async Task CreateSerialAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); created!.Attributes.Count.Should().Be(1); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 100 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 100 }); allAttributes.Records.First().As>() .Document?.Name.Should().BeEquivalentTo(serialAttributeCreateRequest.Name); @@ -1840,13 +1840,13 @@ public async Task CreateSerialAttribute_ValidationError() }; (AttributeConfigurationViewModel _, ValidationErrorResponse errors) = - await _eavService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); errors.Should().BeOfType(); errors.Errors.Should().Contain(x => x.Value.Contains("Increment value must not be negative or 0")); errors.Errors.Should().Contain(x => x.Value.Contains("Statring number must not be negative")); serialAttributeCreateRequest.Increment = -1; - (_, errors) = await _eavService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); + (_, errors) = await _eavEntityInstanceService.CreateAttribute(serialAttributeCreateRequest, CancellationToken.None); errors.Errors.Should().Contain(x => x.Value.Contains("Increment value must not be negative or 0")); } @@ -1885,7 +1885,7 @@ public async Task UpdateSerialAttribute_Success() }; (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); // create update request and change attribute var serialAttributeUpdateRequest = new SerialAttributeConfigurationUpdateRequest @@ -1904,7 +1904,7 @@ public async Task UpdateSerialAttribute_Success() Increment = 5 }; - (AttributeConfigurationViewModel? updatedAttribute, ProblemDetails _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updatedAttribute, ProblemDetails _) = await _eavEntityInstanceService.UpdateAttribute( created.Attributes.FirstOrDefault().AttributeConfigurationId, serialAttributeUpdateRequest, CancellationToken.None @@ -1961,9 +1961,9 @@ public async Task CreateEntityInstanceWithSerialAttributes_Success() }; (EntityConfigurationViewModel? entityConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); - (EntityInstanceViewModel result, ProblemDetails _) = await _eavService.CreateEntityInstance( + (EntityInstanceViewModel result, ProblemDetails _) = await _eavEntityInstanceService.CreateEntityInstance( new EntityInstanceCreateRequest { EntityConfigurationId = entityConfig.Id, @@ -2003,7 +2003,7 @@ public async Task CreateEntityInstanceWithSerialAttributes_Success() .As().Value.Should().Be(serialAttributeCreateRequest.StartingNumber); // create another entity instance - (result, _) = await _eavService.CreateEntityInstance(new EntityInstanceCreateRequest + (result, _) = await _eavEntityInstanceService.CreateEntityInstance(new EntityInstanceCreateRequest { EntityConfigurationId = entityConfig.Id, Attributes = new List @@ -2057,7 +2057,7 @@ public async Task AddAttributeToEntityConfiguration_Success() }; (EntityConfigurationViewModel? createdEntityConfiguration, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); var numberAttribute = new NumberAttributeConfigurationCreateUpdateRequest { @@ -2078,17 +2078,17 @@ public async Task AddAttributeToEntityConfiguration_Success() }; (AttributeConfigurationViewModel? createdAttribute, _) = - await _eavService.CreateAttribute(numberAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(numberAttribute, CancellationToken.None); createdAttribute.Should().NotBeNull(); - await _eavService.AddAttributeToEntityConfiguration( + await _eavEntityInstanceService.AddAttributeToEntityConfiguration( createdAttribute.Id, createdEntityConfiguration.Id, CancellationToken.None ); // check that attribute is added - EntityConfigurationViewModel updatedEntityConfiguration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel updatedEntityConfiguration = await _eavEntityInstanceService.GetEntityConfiguration( createdEntityConfiguration.Id ); @@ -2108,7 +2108,7 @@ public async Task AddAttributeToEntityConfiguration_MachineNamesAreNotUnique() (configCreateRequest.Attributes[0] as AttributeConfigurationCreateUpdateRequest)!.MachineName!; (EntityConfigurationViewModel? createdEntityConfiguration, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); var numberAttribute = new NumberAttributeConfigurationCreateUpdateRequest { @@ -2129,11 +2129,11 @@ public async Task AddAttributeToEntityConfiguration_MachineNamesAreNotUnique() }; (AttributeConfigurationViewModel? createdAttribute, _) = - await _eavService.CreateAttribute(numberAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(numberAttribute, CancellationToken.None); createdAttribute.Should().NotBeNull(); (EntityConfigurationViewModel? entityConfig, ProblemDetails? error) = - await _eavService.AddAttributeToEntityConfiguration( + await _eavEntityInstanceService.AddAttributeToEntityConfiguration( createdAttribute.Id, createdEntityConfiguration.Id, CancellationToken.None @@ -2153,7 +2153,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2163,7 +2163,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var playerMaxIndex = attributesRequest.FindIndex(a => a.ConfigurationAttributeMachineName == changedAttributeName); @@ -2180,7 +2180,7 @@ public async Task UpdateInstance_UpdateAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2195,7 +2195,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2205,7 +2205,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var playerMaxIndex = attributesRequest.FindIndex(a => a.ConfigurationAttributeMachineName == changedAttributeName); @@ -2222,7 +2222,7 @@ public async Task UpdateInstance_UpdateAttribute_FailValidation() }; (EntityInstanceViewModel updatedInstance, ProblemDetails validationErrors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2237,7 +2237,7 @@ public async Task UpdateInstance_AddAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2247,7 +2247,7 @@ public async Task UpdateInstance_AddAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2264,7 +2264,7 @@ public async Task UpdateInstance_AddAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2279,7 +2279,7 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2289,7 +2289,7 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2305,13 +2305,13 @@ public async Task CreateInstance_NumberOfItemsWithAttributeUpdated_Success() Id = createdInstance.Id }; - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); ProjectionQueryResult attributeConfigurations = - await _eavService.ListAttributes( + await _eavEntityInstanceService.ListAttributes( new ProjectionQuery { Filters = new List @@ -2345,7 +2345,7 @@ public async Task UpdateInstance_AddNumberAttribute_InvalidNumberType() (numberAttributeConfig as NumberAttributeConfigurationCreateUpdateRequest)!.NumberType = NumberAttributeType.Integer; - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2363,7 +2363,7 @@ public async Task UpdateInstance_AddNumberAttribute_InvalidNumberType() ); (EntityInstanceViewModel instance, ProblemDetails error) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); instance.Should().BeNull(); error.As().Errors.Should() .Contain(x => x.Value.Contains("Value is not an integer value")); @@ -2376,7 +2376,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2386,7 +2386,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest.Add(new NumberAttributeInstanceCreateUpdateRequest { @@ -2403,7 +2403,7 @@ public async Task UpdateInstance_AddAttribute_IgnoreAttributeNotInConfig() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2418,7 +2418,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2428,7 +2428,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); var updateRequest = new EntityInstanceUpdateRequest { @@ -2442,7 +2442,7 @@ public async Task UpdateInstance_RemoveAttribute_Success() }; (EntityInstanceViewModel updatedInstance, _) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2457,7 +2457,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); @@ -2467,7 +2467,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() List attributesRequest = entityInstanceCreateRequest.Attributes; (EntityInstanceViewModel createdInstance, _) = - await _eavService.CreateEntityInstance(entityInstanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(entityInstanceCreateRequest); attributesRequest = attributesRequest .Where(a => a.ConfigurationAttributeMachineName != changedAttributeName).ToList(); @@ -2483,7 +2483,7 @@ public async Task UpdateInstance_RemoveAttribute_FailValidation() }; (EntityInstanceViewModel updatedInstance, ProblemDetails errors) = - await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), updateRequest, CancellationToken.None ); @@ -2512,7 +2512,7 @@ public async Task CreateNumberAttributeAsReference_Success() (AttributeConfigurationViewModel? priceAttributeCreated, _) = - await _eavService.CreateAttribute(priceAttribute, CancellationToken.None); + await _eavEntityInstanceService.CreateAttribute(priceAttribute, CancellationToken.None); var entityConfigurationCreateRequest = new EntityConfigurationCreateRequest { @@ -2538,13 +2538,13 @@ public async Task CreateNumberAttributeAsReference_Success() } }; - _ = await _eavService.CreateEntityConfiguration( + _ = await _eavEntityInstanceService.CreateEntityConfiguration( entityConfigurationCreateRequest, CancellationToken.None ); ProjectionQueryResult allAttributes = - await _eavService.ListAttributes(new ProjectionQuery { Limit = 1000 }); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery { Limit = 1000 }); allAttributes.Records.Count.Should().Be(2); } @@ -2554,12 +2554,12 @@ public async Task CreateInstanceAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); @@ -2569,7 +2569,7 @@ public async Task CreateInstanceAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavService.CreateEntityInstance(instanceCreateRequest); + await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -2581,7 +2581,7 @@ public async Task CreateInstanceAndQuery() Filters = new List { new("Id", FilterOperator.Equal, createdInstance.Id) } }; - await _eavService + await _eavEntityInstanceService .QueryInstances(createdConfiguration.Id, query); } @@ -2698,10 +2698,10 @@ public async Task AddAttributeMetadata_Success() (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); createdConfig.Should().NotBeNull(); - AttributeConfigurationViewModel attribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel attribute = await _eavEntityInstanceService.GetAttribute( createdConfig!.Attributes[0].AttributeConfigurationId, CancellationToken.None ); @@ -2712,7 +2712,7 @@ public async Task AddAttributeMetadata_Success() // check projections ProjectionQueryResult attributes = - await _eavService.ListAttributes(new ProjectionQuery()); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery()); attributes.Records.First().Document!.Metadata.Should().Be(attribute.Metadata); } @@ -2748,12 +2748,12 @@ public async Task UpdateAttributeMetadata_Success() (EntityConfigurationViewModel? createdConfig, _) = - await _eavService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(entityConfigurationCreateRequest, CancellationToken.None); createdConfig.Should().NotBeNull(); // update attribute metadata priceAttribute.Metadata = "updated metadata"; - (AttributeConfigurationViewModel? updatedAttribute, _) = await _eavService.UpdateAttribute( + (AttributeConfigurationViewModel? updatedAttribute, _) = await _eavEntityInstanceService.UpdateAttribute( createdConfig.Attributes[0].AttributeConfigurationId, priceAttribute, CancellationToken.None @@ -2761,7 +2761,7 @@ public async Task UpdateAttributeMetadata_Success() updatedAttribute.Should().NotBeNull(); - AttributeConfigurationViewModel attribute = await _eavService.GetAttribute( + AttributeConfigurationViewModel attribute = await _eavEntityInstanceService.GetAttribute( updatedAttribute.Id, CancellationToken.None ); @@ -2770,7 +2770,7 @@ public async Task UpdateAttributeMetadata_Success() // check projections ProjectionQueryResult attributesList = - await _eavService.ListAttributes(new ProjectionQuery()); + await _eavEntityInstanceService.ListAttributes(new ProjectionQuery()); attributesList.Records.First().Document!.Metadata.Should().Be(priceAttribute.Metadata); } @@ -2855,13 +2855,13 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty // Act (EntityConfigurationViewModel? created, _) = - await _eavService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); + await _eavEntityInstanceService.CreateEntityConfiguration(configCreateRequest, CancellationToken.None); // Assert // Check domain models var createdArrayAttributeRef = created?.Attributes.First(); createdArrayAttributeRef.Should().NotBeNull(); - var createdAttribute = await _eavService.GetAttribute(createdArrayAttributeRef!.AttributeConfigurationId) as ArrayAttributeConfigurationViewModel; + var createdAttribute = await _eavEntityInstanceService.GetAttribute(createdArrayAttributeRef!.AttributeConfigurationId) as ArrayAttributeConfigurationViewModel; createdAttribute.Name.Should().BeEquivalentTo(arrayAttribute.Name); createdAttribute.Description.Should().BeEquivalentTo(arrayAttribute.Description); @@ -2870,7 +2870,7 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty // Check element config var elementAttributeId = createdAttribute.ItemsAttributeConfigurationId; elementAttributeId.Should().NotBeEmpty(); - var createdElementAttribute = await _eavService.GetAttribute(elementAttributeId); + var createdElementAttribute = await _eavEntityInstanceService.GetAttribute(elementAttributeId); var defaultConfigToCompare = DefaultAttributeConfigurationFactory.GetDefaultConfiguration(type, createdElementAttribute.MachineName, From c0d82097f00a0b7fb013c31fa88bcbca65ef4365 Mon Sep 17 00:00:00 2001 From: Dmytro Honcharenko Date: Tue, 1 Aug 2023 16:50:48 +0300 Subject: [PATCH 5/9] Fixed tree view --- .../ProjectionDocumentSchemaFactory.cs | 15 ++++++ CloudFabric.EAV.Service/EAVCategoryService.cs | 54 ++++++++++++++----- .../EntityInstanceMappingProfile.cs | 7 +-- ...ntityInstanceFromDictionaryDeserializer.cs | 11 +++- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs index f5e978e..e94771d 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs @@ -44,10 +44,25 @@ List attributeConfigurations } ); + schema.Properties.Add( + new ProjectionDocumentPropertySchema + { + PropertyName = "MachineName", + PropertyType = TypeCode.String, + IsKey = false, + IsSearchable = true, + IsRetrievable = true, + IsFilterable = true, + IsSortable = false, + IsFacetable = false + } + ); + schema.Properties.Add( ProjectionAttributesSchemaFactory.GetCategoryPathsAttributeSchema() ); + schema.Properties.Add( new ProjectionDocumentPropertySchema { diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs index b4d8e84..2c1f97e 100644 --- a/CloudFabric.EAV.Service/EAVCategoryService.cs +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -609,55 +609,81 @@ await QueryInstances(tree.EntityConfigurationId, cancellationToken ).ConfigureAwait(false); - var treeElements = treeElementsQueryResult.Records.Select(x => x.Document!).ToList(); + var treeElements = treeElementsQueryResult.Records + .Select(x => x.Document!) + .Select(x => + { + x.CategoryPaths = x.CategoryPaths.Where(cp => cp.TreeId == treeId).ToList(); + return x; + }).ToList(); + + + return BuildTreeView(treeElements, notDeeperThanCategoryId); + + } + + private List BuildTreeView(List categories, Guid? notDeeperThanCategoryId) + { int searchedLevelPathLenght; if (notDeeperThanCategoryId != null) { - var category = treeElements.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); + var category = categories.FirstOrDefault(x => x.Id == notDeeperThanCategoryId); if (category == null) { throw new NotFoundException("Category not found"); } - searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length; + searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault()!.Path.Length; - treeElements = treeElements - .Where(x => x.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId)!.Path.Length <= searchedLevelPathLenght).ToList(); + categories = categories + .Where(x => x.CategoryPaths.FirstOrDefault()!.Path.Length <= searchedLevelPathLenght).ToList(); } var treeViewModel = new List(); // Go through each instance once - foreach (CategoryViewModel treeElement in treeElements - .OrderBy(x => x.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path.Length)) + foreach (CategoryViewModel treeElement in categories + .OrderBy(x => x.CategoryPaths.FirstOrDefault()?.Path.Length)) { var treeElementViewModel = _mapper.Map(treeElement); - var categoryPath = treeElement.CategoryPaths.FirstOrDefault(cp => cp.TreeId == treeId)?.Path; + var categoryPath = treeElement.CategoryPaths.FirstOrDefault()?.Path; + // If categoryPath is empty, that this is a root model -> add it directly to the tree if (string.IsNullOrEmpty(categoryPath)) { treeViewModel.Add(treeElementViewModel); } else { + // Else split categoryPath and extract each parent machine name IEnumerable categoryPathElements = categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x)); + + // Go through each element of the path, remembering where we are atm, and passing current version of treeViewModel + // Applies an accumulator function over a sequence of paths. EntityTreeInstanceViewModel? currentLevel = null; - categoryPathElements.Aggregate(treeViewModel, - (acc, pathComponent) => + + categoryPathElements.Aggregate( + treeViewModel, // initial value + (treeViewModelCurrent, pathComponent) => // apply function to a sequence { + // try to find parent with current pathComponent in the current version of treeViewModel in case + // it had already been added to our tree model on previous iterations EntityTreeInstanceViewModel? parent = - acc.FirstOrDefault(y => y.Id.ToString() == pathComponent); + treeViewModelCurrent.FirstOrDefault(y => y.MachineName == pathComponent); + + // If it is not still there -> find it in the global list of categories and add to our treeViewModel if (parent == null) { - EntityInstanceViewModel? parentInstance = treeElements.FirstOrDefault(x => x.Id.ToString() == pathComponent); + CategoryViewModel? parentInstance = categories.FirstOrDefault(y => y.MachineName == pathComponent); parent = _mapper.Map(parentInstance); - acc.Add(parent); + treeViewModelCurrent.Add(parent); } + // Move to the next level currentLevel = parent; return parent.Children; } @@ -665,8 +691,8 @@ await QueryInstances(tree.EntityConfigurationId, currentLevel?.Children.Add(treeElementViewModel); } } - return treeViewModel; + } /// diff --git a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs index 6e97c7b..3ac9ed1 100644 --- a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs +++ b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs @@ -15,9 +15,7 @@ public EntityInstanceProfile() CreateMap(); CreateMap(); - CreateMap().ForMember(o => o.Children, - opt => opt.MapFrom(_ => new List()) - ); + CreateMap(); CreateMap(); @@ -25,6 +23,9 @@ public EntityInstanceProfile() CreateMap(); CreateMap(); CreateMap(); + CreateMap().ForMember(o => o.Children, + opt => opt.MapFrom(_ => new List()) + ); CreateMap(); CreateMap(); diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs index cd69a21..1876bea 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs @@ -37,6 +37,14 @@ internal List ParseCategoryPaths(object? paths) { categoryPath.TreeId = (Guid)pathItem.Value; } + else if (pathItem.Key == "parentId") + { + categoryPath.ParentId = (Guid)pathItem.Value; + } + else if (pathItem.Key == "parentMachineName") + { + categoryPath.ParentMachineName = (string)pathItem.Value; + } } categoryPaths.Add(categoryPath); @@ -254,7 +262,8 @@ public override CategoryViewModel Deserialize( .ToList(), CategoryPaths = record.ContainsKey("CategoryPaths") ? ParseCategoryPaths(record["CategoryPaths"]) - : new List() + : new List(), + MachineName = (string)record["MachineName"]! }; return category; } From c64bff8f8934a5c8af26505cde190ec98fa3fb74 Mon Sep 17 00:00:00 2001 From: Dmitriy Melnychenko Date: Tue, 29 Aug 2023 17:11:14 +0300 Subject: [PATCH 6/9] Update EventSoursing nugets. Fix tests is needed --- .../CloudFabric.EAV.Domain.csproj | 4 +- .../CloudFabric.EAV.Service.csproj | 2 +- .../BaseQueryTests/BaseQueryTests.cs | 67 ++++++++++++++++--- .../CategoryTests/CategoryTestsPostgresql.cs | 21 ++++-- ...ategoryTestsPostgresqlWithElasticSearch.cs | 21 ++++-- .../CloudFabric.EAV.Tests.csproj | 11 +-- .../EntityInstanceQueryingTests.cs | 1 - .../EntityInstanceQueryingTestsInMemory.cs | 17 ++++- .../EntityInstanceQueryingTestsPostgresql.cs | 23 +++++-- ...ueryingTestsPostgresqlWithElasticSearch.cs | 21 ++++-- .../JsonSerializationTests.cs | 22 ++++-- CloudFabric.EAV.Tests/Tests.cs | 59 +++++++++++++--- 12 files changed, 219 insertions(+), 50 deletions(-) diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj index 697d9f5..4dc9094 100644 --- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj +++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj index ca82788..b8c582a 100644 --- a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj +++ b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj @@ -13,7 +13,7 @@ - + diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index 74cbde1..a3bd00b 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -3,15 +3,19 @@ using AutoMapper; +using CloudFabric.EAV.Domain.Models; using CloudFabric.EAV.Domain.Projections.AttributeConfigurationProjection; +using CloudFabric.EAV.Domain.Projections.EntityConfigurationProjection; using CloudFabric.EAV.Domain.Projections.EntityInstanceProjection; using CloudFabric.EAV.Service; using CloudFabric.EventSourcing.Domain; using CloudFabric.EventSourcing.EventStore; using CloudFabric.EventSourcing.EventStore.Persistence; using CloudFabric.Projections; +using CloudFabric.Projections.Worker; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.BaseQueryTests; @@ -20,16 +24,18 @@ public abstract class BaseQueryTests { protected EAVEntityInstanceService _eavEntityInstanceService; protected EAVCategoryService _eavCategoryService; - + protected IEventStore _eventStore; + protected IStore _store; protected ILogger _eiLogger; protected ILogger _cLogger; protected virtual TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(0); protected abstract IEventStore GetEventStore(); + protected abstract IStore GetStore(); protected abstract ProjectionRepositoryFactory GetProjectionRepositoryFactory(); - protected abstract IEventsObserver GetEventStoreEventsObserver(); + protected abstract EventsObserver GetEventStoreEventsObserver(); [TestInitialize] public async Task SetUp() @@ -37,13 +43,13 @@ public async Task SetUp() var loggerFactory = new LoggerFactory(); _eiLogger = loggerFactory.CreateLogger(); _cLogger = loggerFactory.CreateLogger(); - + var eiConfiguration = new MapperConfiguration(cfg => { cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); } ); - + var cConfiguration = new MapperConfiguration(cfg => { cfg.AddMaps(Assembly.GetAssembly(typeof(EAVCategoryService))); @@ -51,33 +57,76 @@ public async Task SetUp() ); IMapper? eiMapper = eiConfiguration.CreateMapper(); IMapper? cMapper = eiConfiguration.CreateMapper(); - _eventStore = GetEventStore(); await _eventStore.Initialize(); + _store = GetStore(); + await _store.Initialize(); + var aggregateRepositoryFactory = new AggregateRepositoryFactory(_eventStore); ProjectionRepositoryFactory projectionRepositoryFactory = GetProjectionRepositoryFactory(); // Projections engine - takes events from events observer and passes them to multiple projection builders - var projectionsEngine = new ProjectionsEngine( - projectionRepositoryFactory.GetProjectionRepository() - ); + var projectionsEngine = new ProjectionsEngine(); projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( projectionRepositoryFactory, aggregateRepositoryFactory ); + var entityConfigurationProjectionBuilder = new EntityConfigurationProjectionBuilder( + projectionRepositoryFactory, aggregateRepositoryFactory + ); + var entityInstanceProjectionBuilder = new EntityInstanceProjectionBuilder( projectionRepositoryFactory, aggregateRepositoryFactory ); projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); + projectionsEngine.AddProjectionBuilder(entityConfigurationProjectionBuilder); projectionsEngine.AddProjectionBuilder(entityInstanceProjectionBuilder); await projectionsEngine.StartAsync("TestInstance"); + var projectionsRebuildProcessor = new ProjectionsRebuildProcessor( + GetProjectionRepositoryFactory().GetProjectionsIndexStateRepository(), + async (string connectionId) => + { + var rebuildProjectionsEngine = new ProjectionsEngine(); + rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); + + var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( + projectionRepositoryFactory, aggregateRepositoryFactory + ); + + var entityConfigurationProjectionBuilder2 = new EntityConfigurationProjectionBuilder( + projectionRepositoryFactory, aggregateRepositoryFactory + ); + + var entityInstanceProjectionBuilder2 = new EntityInstanceProjectionBuilder( + projectionRepositoryFactory, aggregateRepositoryFactory + ); + + rebuildProjectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(entityConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(entityInstanceProjectionBuilder2); + + return rebuildProjectionsEngine; + }, + NullLogger.Instance + ); + + var attributeConfigurationProjectionRepository = + projectionRepositoryFactory.GetProjectionRepository(); + await attributeConfigurationProjectionRepository.EnsureIndex(); + + var entityConfigurationProjectionRepository = + projectionRepositoryFactory.GetProjectionRepository(); + await entityConfigurationProjectionRepository.EnsureIndex(); + + await projectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + _eavEntityInstanceService = new EAVEntityInstanceService( _eiLogger, eiMapper, @@ -90,7 +139,7 @@ public async Task SetUp() projectionRepositoryFactory, new EventUserInfo(Guid.NewGuid()) ); - + _eavCategoryService = new EAVCategoryService( _cLogger, cMapper, diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs index d659254..17283c1 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.Postgresql; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.CategoryTests; @@ -11,6 +12,7 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public class CategoryTestsPostgresql : CategoryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public CategoryTestsPostgresql() { @@ -22,9 +24,15 @@ public CategoryTestsPostgresql() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_items_store" ); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(new LoggerFactory(), connectionString); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + using var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } protected override IEventStore GetEventStore() @@ -32,9 +40,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs index d630070..fd92f97 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresqlWithElasticSearch.cs @@ -12,6 +12,7 @@ namespace CloudFabric.EAV.Tests.CategoryTests; public class CategoryTestsPostgresqlWithElasticSearch : CategoryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public CategoryTestsPostgresqlWithElasticSearch() { @@ -23,7 +24,8 @@ public CategoryTestsPostgresqlWithElasticSearch() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); _projectionRepositoryFactory = new ElasticSearchProjectionRepositoryFactory( @@ -32,8 +34,14 @@ public CategoryTestsPostgresqlWithElasticSearch() "", "", ""), - new LoggerFactory() + new LoggerFactory(), + true ); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } protected override TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(1000); @@ -43,9 +51,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj index 7778ebd..2c485c4 100644 --- a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj +++ b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj @@ -11,11 +11,12 @@ - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs index a522cdd..6d992e3 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs @@ -50,7 +50,6 @@ public async Task TestCreateInstanceAndQuery() createdInstance.Attributes.Should() .BeEquivalentTo(instanceCreateRequest.Attributes, x => x.Excluding(w => w.ValueType)); - var query = new ProjectionQuery { Filters = new List { new("Id", FilterOperator.Equal, createdInstance.Id) } diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs index 7778a89..5c2fa4b 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsInMemory.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.InMemory; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; @@ -11,16 +12,21 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsInMemory : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsInMemory() { + var loggerFactory = new LoggerFactory(); + _eventStore = new InMemoryEventStore(new Dictionary<(Guid, string), List>()); - _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(); + _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(loggerFactory); + _store = new InMemoryStore(new Dictionary<(string, string), string>()); + _logger = loggerFactory.CreateLogger(); } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override EventsObserver GetEventStoreEventsObserver() { - return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore); + return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore, _logger); } protected override IEventStore GetEventStore() @@ -28,6 +34,11 @@ protected override IEventStore GetEventStore() return _eventStore; } + protected override IStore GetStore() + { + return _store; + } + protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() { return _projectionRepositoryFactory; diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs index f07e4de..4bdeb52 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresql.cs @@ -3,6 +3,7 @@ using CloudFabric.Projections; using CloudFabric.Projections.Postgresql; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; @@ -11,6 +12,7 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsPostgresql : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsPostgresql() { @@ -22,9 +24,17 @@ public EntityInstanceQueryingTestsPostgresql() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + + var loggerFactory = new LoggerFactory(); + + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(loggerFactory, connectionString); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + _logger = loggerFactory.CreateLogger(); } protected override IEventStore GetEventStore() @@ -32,9 +42,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs index be3ed0c..d072db6 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTestsPostgresqlWithElasticSearch.cs @@ -12,6 +12,7 @@ namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; public class EntityInstanceQueryingTestsPostgresqlWithElasticSearch : EntityInstanceQueryingTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public EntityInstanceQueryingTestsPostgresqlWithElasticSearch() { @@ -23,17 +24,24 @@ public EntityInstanceQueryingTestsPostgresqlWithElasticSearch() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); + var loggerFactory = new LoggerFactory(); + _projectionRepositoryFactory = new ElasticSearchProjectionRepositoryFactory( new ElasticSearchBasicAuthConnectionSettings( "http://127.0.0.1:9200", "", "", ""), - new LoggerFactory() + loggerFactory ); + + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + + _logger = loggerFactory.CreateLogger(); } protected override TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(1000); @@ -43,9 +51,14 @@ protected override IEventStore GetEventStore() return _eventStore; } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override IStore GetStore() + { + return _store; + } + + protected override EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore, _logger); } protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index f7ebd91..eee60b7 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -13,6 +13,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CloudFabric.EAV.Tests; @@ -21,16 +22,24 @@ namespace CloudFabric.EAV.Tests; public class JsonSerializationTests : BaseQueryTests.BaseQueryTests { private readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + private readonly ILogger _logger; public JsonSerializationTests() { - _eventStore = new InMemoryEventStore(new Dictionary<(Guid, string), List>()); - _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(); + _eventStore = new InMemoryEventStore( + new Dictionary<(Guid, string), List>() + ); + _projectionRepositoryFactory = new InMemoryProjectionRepositoryFactory(new LoggerFactory()); + + _store = new InMemoryStore(new Dictionary<(string, string), string>()); + + using var loggerFactory = new LoggerFactory(); + _logger = loggerFactory.CreateLogger(); } - protected override IEventsObserver GetEventStoreEventsObserver() + protected override EventsObserver GetEventStoreEventsObserver() { - return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore); + return new InMemoryEventStoreEventObserver((InMemoryEventStore)_eventStore, _logger); } protected override IEventStore GetEventStore() @@ -38,6 +47,11 @@ protected override IEventStore GetEventStore() return _eventStore; } + protected override IStore GetStore() + { + return _store; + } + protected override ProjectionRepositoryFactory GetProjectionRepositoryFactory() { return _projectionRepositoryFactory; diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index e739f27..ed76ceb 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -23,11 +23,13 @@ using CloudFabric.Projections.InMemory; using CloudFabric.Projections.Postgresql; using CloudFabric.Projections.Queries; +using CloudFabric.Projections.Worker; using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; // ReSharper disable AsyncConverter.ConfigureAwaitHighlighting @@ -42,6 +44,7 @@ public class Tests private EAVEntityInstanceService _eavEntityInstanceService; private IEventStore _eventStore; + private IStore _store; private ILogger _eiLogger; private PostgresqlProjectionRepositoryFactory _projectionRepositoryFactory; private IMapper _mapper; @@ -67,18 +70,19 @@ public async Task SetUp() _eventStore = new PostgresqlEventStore( connectionString, - "eav_tests_event_store" + "eav_tests_event_store", + "eav_tests_item_store" ); + _store = new PostgresqlStore(connectionString, "eav_tests_item_store"); + await _eventStore.Initialize(); _aggregateRepositoryFactory = new AggregateRepositoryFactory(_eventStore); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(connectionString); + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(new LoggerFactory(), connectionString); // Projections engine - takes events from events observer and passes them to multiple projection builders - var projectionsEngine = new ProjectionsEngine( - _projectionRepositoryFactory.GetProjectionRepository() - ); + var projectionsEngine = new ProjectionsEngine(); projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( @@ -91,10 +95,42 @@ public async Task SetUp() projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); projectionsEngine.AddProjectionBuilder(ordersListProjectionBuilder); + var ProjectionsRebuildProcessor = new ProjectionsRebuildProcessor( + _projectionRepositoryFactory.GetProjectionsIndexStateRepository(), + async (string connectionId) => + { + var rebuildProjectionsEngine = new ProjectionsEngine(); + rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); - await projectionsEngine.StartAsync("TestInstance"); + var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( + _projectionRepositoryFactory, _aggregateRepositoryFactory + ); + + var ordersListProjectionBuilder2 = new EntityConfigurationProjectionBuilder( + _projectionRepositoryFactory, _aggregateRepositoryFactory + ); + rebuildProjectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder2); + rebuildProjectionsEngine.AddProjectionBuilder(ordersListProjectionBuilder2); + + return rebuildProjectionsEngine; + }, + NullLogger.Instance + ); + + var attributeConfigurationProjectionRepository = + _projectionRepositoryFactory.GetProjectionRepository(); + await attributeConfigurationProjectionRepository.EnsureIndex(); + + var entityConfigurationProjectionRepository = + _projectionRepositoryFactory.GetProjectionRepository(); + await entityConfigurationProjectionRepository.EnsureIndex(); + + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + + await projectionsEngine.StartAsync("TestInstance"); + _eavEntityInstanceService = new EAVEntityInstanceService( _eiLogger, _mapper, @@ -3026,11 +3062,16 @@ private async Task CreateSimpleArrayOfTypesAttribute_Success(EavAttributeType ty private IProjectionRepository GetProjectionRebuildStateRepository() { - return new InMemoryProjectionRepository(); + return new InMemoryProjectionRepository(new LoggerFactory()); } - private IEventsObserver GetEventStoreEventsObserver() + private EventsObserver GetEventStoreEventsObserver() { - return new PostgresqlEventStoreEventObserver((PostgresqlEventStore)_eventStore); + var loggerFactory = new LoggerFactory(); + + return new PostgresqlEventStoreEventObserver( + (PostgresqlEventStore)_eventStore, + loggerFactory.CreateLogger() + ); } } From 311eb900fcfbbc777b26d0e0ac89075323159f56 Mon Sep 17 00:00:00 2001 From: Sergey Ustimenko Date: Wed, 30 Aug 2023 18:31:09 +0300 Subject: [PATCH 7/9] Update eventsourcing library to latest --- ...V.Domain.LocalEventSourcingPackages.csproj | 2 +- .../CloudFabric.EAV.Domain.csproj | 4 ++-- ...AttributeConfigurationProjectionBuilder.cs | 5 +++-- .../EntityConfigurationProjectionBuilder.cs | 5 +++-- .../EntityInstanceProjectionBuilder.cs | 5 +++-- ...V.Models.LocalEventSourcingPackages.csproj | 2 +- .../CloudFabric.EAV.Service.csproj | 2 +- CloudFabric.EAV.Service/EAVCategoryService.cs | 4 ---- CloudFabric.EAV.Service/EAVService.cs | 20 +++++++++++++++++-- .../BaseQueryTests/BaseQueryTests.cs | 18 +++++++++-------- .../CategoryTests/CategoryTests.cs | 4 ++++ .../CloudFabric.EAV.Tests.csproj | 12 +++++------ .../JsonSerializationTests.cs | 4 ++++ CloudFabric.EAV.Tests/Tests.cs | 8 ++++---- 14 files changed, 60 insertions(+), 35 deletions(-) diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj index 378aa65..2d59764 100644 --- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj +++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj @@ -21,7 +21,7 @@ - + diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj index 4dc9094..58c7d33 100644 --- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj +++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs index b2827e4..8bc5d8e 100644 --- a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs @@ -26,8 +26,9 @@ public class AttributeConfigurationProjectionBuilder : ProjectionBuilder { public AttributeConfigurationProjectionBuilder( - ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _ - ) : base(projectionRepositoryFactory) + ProjectionRepositoryFactory projectionRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { } diff --git a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs index 8a96c1d..1dded58 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs @@ -15,8 +15,9 @@ public class EntityConfigurationProjectionBuilder : ProjectionBuilder> { public EntityConfigurationProjectionBuilder( - ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _ - ) : base(projectionRepositoryFactory) + ProjectionRepositoryFactory projectionRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { } diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs index e0804e9..fe2393f 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs @@ -42,8 +42,9 @@ public class EntityInstanceProjectionBuilder : ProjectionBuilder, public EntityInstanceProjectionBuilder( ProjectionRepositoryFactory projectionRepositoryFactory, - AggregateRepositoryFactory aggregateRepositoryFactory - ) : base(projectionRepositoryFactory) + AggregateRepositoryFactory aggregateRepositoryFactory, + ProjectionOperationIndexSelector indexSelector + ) : base(projectionRepositoryFactory, indexSelector) { _aggregateRepositoryFactory = aggregateRepositoryFactory; } diff --git a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj index cf7fb5d..4cd4100 100644 --- a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj +++ b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj @@ -20,7 +20,7 @@ - + diff --git a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj index b8c582a..f7a353d 100644 --- a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj +++ b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj @@ -13,7 +13,7 @@ - + diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs index 2c1f97e..e9cdafa 100644 --- a/CloudFabric.EAV.Service/EAVCategoryService.cs +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -404,11 +404,7 @@ await GetAttributeConfigurationsForEntityConfiguration( return (null, new ValidationErrorResponse(validationErrors))!; } - ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory - .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); - await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken) .ConfigureAwait(false); diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index c778d38..a1fbbb0 100644 --- a/CloudFabric.EAV.Service/EAVService.cs +++ b/CloudFabric.EAV.Service/EAVService.cs @@ -590,7 +590,6 @@ await CreateAttribute( new ValidationErrorResponse(entityConfiguration.MachineName, entityValidationErrors.ToArray())); } - await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); await _entityConfigurationRepository.SaveAsync( _userInfo, @@ -598,6 +597,8 @@ await _entityConfigurationRepository.SaveAsync( cancellationToken ).ConfigureAwait(false); + await EnsureProjectionIndexForEntityConfiguration(entityConfiguration); + return (_mapper.Map(entityConfiguration), null); } @@ -746,13 +747,28 @@ await CreateAttribute( new ValidationErrorResponse(entityConfiguration.MachineName, entityValidationErrors.ToArray())); } - await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); + //await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); + await EnsureProjectionIndexForEntityConfiguration(entityConfiguration); + return (_mapper.Map(entityConfiguration), null); } + private async Task EnsureProjectionIndexForEntityConfiguration(EntityConfiguration entityConfiguration) + { + // when entity configuration is created or updated, we need to create a projections index for it. EnsureIndex + // will just create a record that such index is needed. Then, it will be picked up by background processor + List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration + ); + var schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); + IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema); + await projectionRepository.EnsureIndex(); + } + public async Task<(EntityConfigurationViewModel?, ProblemDetails?)> AddAttributeToEntityConfiguration( Guid attributeId, Guid entityConfigurationId, diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index a3bd00b..f848318 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -37,6 +37,8 @@ public abstract class BaseQueryTests protected abstract ProjectionRepositoryFactory GetProjectionRepositoryFactory(); protected abstract EventsObserver GetEventStoreEventsObserver(); + protected ProjectionsRebuildProcessor ProjectionsRebuildProcessor; + [TestInitialize] public async Task SetUp() { @@ -72,15 +74,15 @@ public async Task SetUp() projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var entityConfigurationProjectionBuilder = new EntityConfigurationProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var entityInstanceProjectionBuilder = new EntityInstanceProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, aggregateRepositoryFactory, ProjectionOperationIndexSelector.Write ); projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); @@ -89,7 +91,7 @@ public async Task SetUp() await projectionsEngine.StartAsync("TestInstance"); - var projectionsRebuildProcessor = new ProjectionsRebuildProcessor( + ProjectionsRebuildProcessor = new ProjectionsRebuildProcessor( GetProjectionRepositoryFactory().GetProjectionsIndexStateRepository(), async (string connectionId) => { @@ -97,15 +99,15 @@ public async Task SetUp() rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var entityConfigurationProjectionBuilder2 = new EntityConfigurationProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var entityInstanceProjectionBuilder2 = new EntityInstanceProjectionBuilder( - projectionRepositoryFactory, aggregateRepositoryFactory + projectionRepositoryFactory, aggregateRepositoryFactory, ProjectionOperationIndexSelector.Write ); rebuildProjectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder2); @@ -125,7 +127,7 @@ public async Task SetUp() projectionRepositoryFactory.GetProjectionRepository(); await entityConfigurationProjectionRepository.EnsureIndex(); - await projectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); _eavEntityInstanceService = new EAVEntityInstanceService( _eiLogger, diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index 205584e..e056dff 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -37,6 +37,8 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + // Create a tree var treeRequest = new CategoryTreeCreateRequest { @@ -253,6 +255,8 @@ public async Task MoveAndGetItemsFromCategory_Success() CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + EntityInstanceCreateRequest itemInstanceRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(itemEntityConfiguration.Id); diff --git a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj index 2c485c4..8ff2bc3 100644 --- a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj +++ b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj @@ -11,12 +11,12 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index eee60b7..22c5a58 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -68,6 +68,8 @@ public async Task TestCreateInstanceMultiLangAndQuery() CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -291,6 +293,8 @@ public async Task CreateCategoryInstance() }, categoryConfigurationRequest.TenantId); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdCategoryConfiguration!.Id ); diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index ed76ceb..b5417c3 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -86,10 +86,10 @@ public async Task SetUp() projectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder = new AttributeConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); var ordersListProjectionBuilder = new EntityConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.Write ); projectionsEngine.AddProjectionBuilder(attributeConfigurationProjectionBuilder); @@ -103,11 +103,11 @@ public async Task SetUp() rebuildProjectionsEngine.SetEventsObserver(GetEventStoreEventsObserver()); var attributeConfigurationProjectionBuilder2 = new AttributeConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.ProjectionRebuild ); var ordersListProjectionBuilder2 = new EntityConfigurationProjectionBuilder( - _projectionRepositoryFactory, _aggregateRepositoryFactory + _projectionRepositoryFactory, ProjectionOperationIndexSelector.ProjectionRebuild ); From 26e7b9ab040b1e0f8f4825fc81c1ba62c861de7b Mon Sep 17 00:00:00 2001 From: Sergey Ustimenko Date: Sat, 2 Sep 2023 17:28:38 +0300 Subject: [PATCH 8/9] Fix tests after upgrading event sourcing lib to 1.2 --- .../ProjectionAttributesSchemaFactory.cs | 1 + CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs | 9 +++++++++ .../EntityInstanceQueryingTests.cs | 2 ++ CloudFabric.EAV.Tests/Tests.cs | 5 ++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs index 8950122..3c7338f 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs @@ -240,6 +240,7 @@ public static ProjectionDocumentPropertySchema GetArrayAttributeSchema( EavAttributeType.HtmlText => null, EavAttributeType.EntityReference => null, EavAttributeType.ValueFromList => null, + EavAttributeType.Money => null, EavAttributeType.LocalizedText => GetLocalizedTextAttributeNestedProperties(), EavAttributeType.DateRange => GetDateAttributeNestedProperties(), EavAttributeType.Image => GetImageAttributeNestedProperties(), diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index f848318..8f7b297 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -61,6 +61,7 @@ public async Task SetUp() IMapper? cMapper = eiConfiguration.CreateMapper(); _eventStore = GetEventStore(); await _eventStore.Initialize(); + await _eventStore.DeleteAll(); _store = GetStore(); await _store.Initialize(); @@ -68,6 +69,14 @@ public async Task SetUp() var aggregateRepositoryFactory = new AggregateRepositoryFactory(_eventStore); ProjectionRepositoryFactory projectionRepositoryFactory = GetProjectionRepositoryFactory(); + var projectionRepository = projectionRepositoryFactory + .GetProjectionRepository( + new ProjectionDocumentSchema + { + SchemaName = "" + } + ); + await projectionRepository.DeleteAll(); // Projections engine - takes events from events observer and passes them to multiple projection builders var projectionsEngine = new ProjectionsEngine(); diff --git a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs index 6d992e3..6772831 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs @@ -33,6 +33,8 @@ public async Task TestCreateInstanceAndQuery() CancellationToken.None ); + await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); + EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( createdConfiguration.Id ); diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index b5417c3..93599be 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -145,7 +145,9 @@ public async Task SetUp() ); } + [TestCleanup] + [TestInitialize] public async Task Cleanup() { await _eventStore.DeleteAll(); @@ -167,8 +169,9 @@ public async Task Cleanup() GetProjectionRebuildStateRepository(); await rebuildStateRepository.DeleteAll(); } - catch + catch(Exception ex) { + Console.WriteLine("Failed to clear projection repository {0} {1}", ex.Message, ex.StackTrace); } } From 2331d8b2cdd195358e223dbd757cd7b88798f715 Mon Sep 17 00:00:00 2001 From: Sergey Ustimenko Date: Sat, 2 Sep 2023 19:43:47 +0300 Subject: [PATCH 9/9] Bump version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5810a44..d000348 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: branches: [ "main" ] env: - PACKAGE_VERSION: 1.2.14 + PACKAGE_VERSION: 1.3.0 jobs: