diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj index edc884f..b91d10f 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/Events/Instance/CategoryCreated.cs b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs deleted file mode 100644 index 9ab9a70..0000000 --- a/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs +++ /dev/null @@ -1,30 +0,0 @@ -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/EntityInstanceCategoryPathUpdated.cs similarity index 85% rename from CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs rename to CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCategoryPathUpdated.cs index 11cfe3d..b6374b9 100644 --- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCategoryPathUpdated.cs @@ -2,15 +2,15 @@ namespace CloudFabric.EAV.Domain.Events.Instance.Entity; -public record EntityCategoryPathChanged : Event +public record EntityInstanceCategoryPathUpdated : Event { // ReSharper disable once UnusedMember.Global // This constructor is required for Event Store to properly deserialize from json - public EntityCategoryPathChanged() + public EntityInstanceCategoryPathUpdated() { } - public EntityCategoryPathChanged(Guid id, + public EntityInstanceCategoryPathUpdated(Guid id, Guid entityConfigurationId, Guid categoryTreeId, string categoryPath, diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCreated.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCreated.cs index b7843e1..48e949d 100644 --- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCreated.cs +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceCreated.cs @@ -5,22 +5,35 @@ namespace CloudFabric.EAV.Domain.Events.Instance.Entity; public record EntityInstanceCreated : Event { + public string? MachineName { get; set; } + + public IReadOnlyCollection? CategoryPaths { get; set; } + + public Guid EntityConfigurationId { get; set; } + + public IReadOnlyCollection Attributes { get; set; } + + public Guid? TenantId { get; set; } + // ReSharper disable once UnusedMember.Global // This constructor is required for Event Store to properly deserialize from json public EntityInstanceCreated() { } - public EntityInstanceCreated(Guid id, Guid entityConfigurationId, List attributes, - Guid? tenantId) - { - TenantId = tenantId; - Attributes = attributes; - EntityConfigurationId = entityConfigurationId; + public EntityInstanceCreated( + Guid id, + Guid entityConfigurationId, + List attributes, + string? machineName, + Guid? tenantId, + List? categoryPaths + ) { AggregateId = id; + EntityConfigurationId = entityConfigurationId; + MachineName = machineName; + Attributes = attributes; + TenantId = tenantId; + CategoryPaths = categoryPaths; } - - public Guid EntityConfigurationId { get; set; } - public List Attributes { get; set; } - public Guid? TenantId { get; set; } } diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceMachineNameUpdated.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceMachineNameUpdated.cs new file mode 100644 index 0000000..c577b3f --- /dev/null +++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityInstanceMachineNameUpdated.cs @@ -0,0 +1,24 @@ +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EventSourcing.EventStore; + +namespace CloudFabric.EAV.Domain.Events.Instance.Entity; + +public record EntityInstanceMachineNameUpdated : Event +{ + // ReSharper disable once UnusedMember.Global + // This constructor is required for Event Store to properly deserialize from json + public EntityInstanceMachineNameUpdated() + { + } + + public EntityInstanceMachineNameUpdated(Guid id, Guid entityConfigurationId, string newMachineName) + { + EntityConfigurationId = entityConfigurationId; + AggregateId = id; + NewMachineName = newMachineName; + } + + public Guid EntityConfigurationId { get; set; } + + public string NewMachineName { get; set; } +} diff --git a/CloudFabric.EAV.Domain/Models/Category.cs b/CloudFabric.EAV.Domain/Models/Category.cs deleted file mode 100644 index b7fb109..0000000 --- a/CloudFabric.EAV.Domain/Models/Category.cs +++ /dev/null @@ -1,46 +0,0 @@ -using CloudFabric.EAV.Domain.Events.Instance.Entity; -using CloudFabric.EventSourcing.EventStore; - -namespace CloudFabric.EAV.Domain.Models; - -public class Category : EntityInstanceBase -{ - public string MachineName { get; set; } - public Category(IEnumerable events) : base(events) - { - } - - public Category(Guid id, - string machineName, - Guid entityConfigurationId, - List attributes, - Guid? 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 - ) : 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 1b2ac03..e248a69 100644 --- a/CloudFabric.EAV.Domain/Models/CategoryPath.cs +++ b/CloudFabric.EAV.Domain/Models/CategoryPath.cs @@ -1,9 +1,9 @@ namespace CloudFabric.EAV.Domain.Models; -public class CategoryPath +public record CategoryPath { public Guid TreeId { get; set; } - public string Path { get; set; } + public string? Path { get; set; } public Guid? ParentId { get; set; } - public string ParentMachineName { get; set; } + public string? ParentMachineName { get; set; } } diff --git a/CloudFabric.EAV.Domain/Models/EntityInstance.cs b/CloudFabric.EAV.Domain/Models/EntityInstance.cs index 50c25fa..84bc638 100644 --- a/CloudFabric.EAV.Domain/Models/EntityInstance.cs +++ b/CloudFabric.EAV.Domain/Models/EntityInstance.cs @@ -1,15 +1,185 @@ +using System.Collections.ObjectModel; + +using CloudFabric.EAV.Domain.Events.Instance.Attribute; +using CloudFabric.EAV.Domain.Events.Instance.Entity; +using CloudFabric.EventSourcing.Domain; using CloudFabric.EventSourcing.EventStore; namespace CloudFabric.EAV.Domain.Models; -public class EntityInstance : EntityInstanceBase +public class EntityInstance : AggregateBase { + /// + /// For multi-tenant applications this can be used to separate records between tenants. + /// + public Guid? TenantId { get; protected set; } + + /// + /// Unique human-readable identifier. Can be used in urls to help with SEO. + /// + public string? MachineName { get; protected set; } + + public override string PartitionKey => EntityConfigurationId.ToString(); + + /// + /// An entity instance can belong to many hierarchy trees, this list will contain a record for each hierarchy tree + /// and a place (path) in it. + /// + public IReadOnlyCollection CategoryPaths { get; protected set; } = + new ReadOnlyCollection(new List()); + + /// + /// All attributes for this EntityInstance are validated against EntityConfiguration's attributes. + /// + public Guid EntityConfigurationId { get; protected set; } + + public IReadOnlyCollection Attributes { get; protected set; } = + new ReadOnlyCollection(new List()); + + protected EntityInstance() + { + } + public EntityInstance(IEnumerable events) : base(events) { } - public EntityInstance(Guid id, Guid entityConfigurationId, List attributes, Guid? tenantId) - : base(id, entityConfigurationId, attributes, tenantId) + public EntityInstance( + Guid id, + Guid entityConfigurationId, + List attributes, + string? machineName, + Guid? tenantId, + List? categoryPaths + ) { + Apply(new EntityInstanceCreated( + id, + entityConfigurationId, + attributes, + machineName, + tenantId, + categoryPaths + )); + } + + public void AddAttributeInstance(AttributeInstance attribute) + { + Apply(new AttributeInstanceAdded(Id, attribute)); + } + + public void UpdateAttributeInstance(AttributeInstance attribute) + { + Apply(new AttributeInstanceUpdated(Id, EntityConfigurationId, attribute)); + } + + public void RemoveAttributeInstance(string attributeMachineName) + { + Apply(new AttributeInstanceRemoved(Id, EntityConfigurationId, attributeMachineName)); + } + + public void UpdateMachineName(string newMachineName) + { + Apply(new EntityInstanceMachineNameUpdated(Id, EntityConfigurationId, newMachineName)); + } + + /// + /// Updates the path of this entity inside one of hierarchy trees is belongs to. + /// + /// Id of existing category tree. + /// Slash-separated path constructed from `machineName`s of parent EntityInstances. + /// Empty for root node. Example: Electronics/Laptops + /// + public void UpdateCategoryPath(Guid treeId, string categoryPath, Guid parentId) + { + Apply(new EntityInstanceCategoryPathUpdated(Id, EntityConfigurationId, treeId, categoryPath, parentId)); + } + + #region Event Handlers + + public void On(EntityInstanceCreated @event) { + Id = @event.AggregateId; + EntityConfigurationId = @event.EntityConfigurationId; + Attributes = new List(@event.Attributes).AsReadOnly(); + MachineName = @event.MachineName; + TenantId = @event.TenantId; + CategoryPaths = @event.CategoryPaths ?? new List().AsReadOnly(); } + + public void On(AttributeInstanceAdded @event) + { + List newCollection = new List(Attributes) { @event.AttributeInstance }; + Attributes = newCollection.AsReadOnly(); + } + + public void On(AttributeInstanceUpdated @event) + { + AttributeInstance? attribute = Attributes.FirstOrDefault(x => + x.ConfigurationAttributeMachineName == @event.AttributeInstance.ConfigurationAttributeMachineName + ); + + if (attribute != null) + { + var newCollection = new List(Attributes); + + newCollection.Remove(attribute); + newCollection.Add(@event.AttributeInstance); + + Attributes = newCollection.AsReadOnly(); + } + } + + public void On(AttributeInstanceRemoved @event) + { + AttributeInstance? attribute = Attributes.FirstOrDefault(x => + x.ConfigurationAttributeMachineName == @event.AttributeMachineName + ); + + if (attribute != null) + { + var newCollection = new List(Attributes); + + newCollection.Remove(attribute); + + Attributes = newCollection.AsReadOnly(); + } + } + + public void On(EntityInstanceCategoryPathUpdated @event) + { + CategoryPath? categoryPath = CategoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId); + + var newCollection = new List(CategoryPaths); + + if (categoryPath != null) + { + newCollection.Remove(categoryPath); + } + + var newCategoryPath = categoryPath != null + ? categoryPath with + { + Path = @event.CategoryPath, + ParentMachineName = @event.ParentMachineName, + ParentId = @event.ParentId + } + : new CategoryPath + { + TreeId = @event.CategoryTreeId, + Path = @event.CategoryPath, + ParentId = @event.ParentId, + ParentMachineName = @event.ParentMachineName + }; + + newCollection.Add(newCategoryPath); + + CategoryPaths = newCollection.AsReadOnly(); + } + + public void On(EntityInstanceMachineNameUpdated @event) + { + MachineName = @event.NewMachineName; + } + + #endregion } diff --git a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs deleted file mode 100644 index 3ffa85a..0000000 --- a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Collections.ObjectModel; - -using CloudFabric.EAV.Domain.Events.Instance.Attribute; -using CloudFabric.EAV.Domain.Events.Instance.Entity; -using CloudFabric.EventSourcing.Domain; -using CloudFabric.EventSourcing.EventStore; - -namespace CloudFabric.EAV.Domain.Models; - -public class EntityInstanceBase : AggregateBase -{ - public EntityInstanceBase(IEnumerable events) : base(events) - { - } - - public EntityInstanceBase(Guid id, Guid entityConfigurationId, List attributes, - Guid? tenantId) - { - Apply(new EntityInstanceCreated(id, entityConfigurationId, attributes, tenantId)); - } - - protected EntityInstanceBase() - { - } - - public override string PartitionKey => EntityConfigurationId.ToString(); - public List CategoryPaths { get; protected set; } - - public Guid EntityConfigurationId { get; protected set; } - - public ReadOnlyCollection Attributes { get; protected set; } - - public Guid? TenantId { get; protected set; } - - public void AddAttributeInstance(AttributeInstance attribute) - { - Apply(new AttributeInstanceAdded(Id, attribute)); - } - - public void UpdateAttributeInstance(AttributeInstance attribute) - { - Apply(new AttributeInstanceUpdated(Id, EntityConfigurationId, attribute)); - } - - public void RemoveAttributeInstance(string attributeMachineName) - { - Apply(new AttributeInstanceRemoved(Id, EntityConfigurationId, attributeMachineName)); - } - - #region Event Handlers - - public void On(EntityInstanceCreated @event) - { - Id = @event.AggregateId; - EntityConfigurationId = @event.EntityConfigurationId; - Attributes = new List(@event.Attributes).AsReadOnly(); - TenantId = @event.TenantId; - CategoryPaths = new List(); - } - - public void ChangeCategoryPath(Guid treeId, string categoryPath, Guid parentId) - { - Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath, parentId)); - } - - 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, - ParentId = @event.ParentId, - ParentMachineName = @event.ParentMachineName - }); - } - else - { - categoryPath.Path = @event.CategoryPath; - categoryPath.ParentMachineName = @event.ParentMachineName; - categoryPath.ParentId = @event.ParentId; - } - } - - - public void On(AttributeInstanceAdded @event) - { - List newCollection = Attributes == null - ? new List() - : new List(Attributes); - newCollection.Add(@event.AttributeInstance); - Attributes = newCollection.AsReadOnly(); - } - - public void On(AttributeInstanceUpdated @event) - { - AttributeInstance? attribute = Attributes?.FirstOrDefault(x => - x.ConfigurationAttributeMachineName == @event.AttributeInstance.ConfigurationAttributeMachineName - ); - - if (attribute != null) - { - var newCollection = new List(Attributes); - - newCollection.Remove(attribute); - newCollection.Add(@event.AttributeInstance); - - Attributes = newCollection.AsReadOnly(); - } - } - - public void On(AttributeInstanceRemoved @event) - { - AttributeInstance? attribute = - Attributes?.FirstOrDefault(x => x.ConfigurationAttributeMachineName == @event.AttributeMachineName); - - if (attribute != null) - { - var newCollection = new List(Attributes); - - newCollection.Remove(attribute); - - Attributes = newCollection.AsReadOnly(); - } - } - - #endregion -} diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs index fe2393f..d920d62 100644 --- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs +++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs @@ -1,7 +1,6 @@ using CloudFabric.EAV.Domain.Events.Instance.Attribute; using CloudFabric.EAV.Domain.Events.Instance.Entity; using CloudFabric.EAV.Domain.Models; -using CloudFabric.EAV.Domain.Models.Attributes; using CloudFabric.EventSourcing.Domain; using CloudFabric.Projections; @@ -31,11 +30,11 @@ namespace CloudFabric.EAV.Domain.Projections.EntityInstanceProjection; /// public class EntityInstanceProjectionBuilder : ProjectionBuilder, IHandleEvent, - IHandleEvent, + //IHandleEvent, // IHandleEvent, IHandleEvent, // IHandleEvent, - IHandleEvent, + IHandleEvent, IHandleEvent> { private readonly AggregateRepositoryFactory _aggregateRepositoryFactory; @@ -118,7 +117,7 @@ await UpdateDocument( ).ConfigureAwait(false); } - public async Task On(EntityCategoryPathChanged @event) + public async Task On(EntityInstanceCategoryPathUpdated @event) { ProjectionDocumentSchema projectionDocumentSchema = await BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( @@ -169,36 +168,8 @@ await BuildProjectionDocumentSchemaForEntityConfigurationIdAsync( { "Id", @event.AggregateId }, { "EntityConfigurationId", @event.EntityConfigurationId }, { "TenantId", @event.TenantId }, - { "CategoryPaths", new List() } - }; - - foreach (AttributeInstance attribute in @event.Attributes) - { - document.Add(attribute.ConfigurationAttributeMachineName, attribute.GetValue()); - } - - await UpsertDocument( - projectionDocumentSchema, - document, - @event.PartitionKey, - @event.Timestamp - ); - } - - 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}, + { "MachineName", @event.MachineName }, + { "CategoryPaths", @event.CategoryPaths } }; foreach (AttributeInstance attribute in @event.Attributes) diff --git a/CloudFabric.EAV.Models/RequestModels/CategoryPathCreateUpdateRequest.cs b/CloudFabric.EAV.Models/RequestModels/CategoryPathCreateUpdateRequest.cs new file mode 100644 index 0000000..953de81 --- /dev/null +++ b/CloudFabric.EAV.Models/RequestModels/CategoryPathCreateUpdateRequest.cs @@ -0,0 +1,10 @@ +namespace CloudFabric.EAV.Models.RequestModels; + +public record CategoryPathCreateUpdateRequest +{ + public Guid TreeId { get; set; } + + public string? Path { get; set; } + + public Guid? ParentId { get; set; } +} diff --git a/CloudFabric.EAV.Models/RequestModels/CategoryTreeCreateRequest.cs b/CloudFabric.EAV.Models/RequestModels/CategoryTreeCreateRequest.cs index 934208a..fd0d3fa 100644 --- a/CloudFabric.EAV.Models/RequestModels/CategoryTreeCreateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/CategoryTreeCreateRequest.cs @@ -2,7 +2,19 @@ namespace CloudFabric.EAV.Models.RequestModels; public class CategoryTreeCreateRequest { + /// + /// Unique human-readable string identifier. + /// public string MachineName { get; set; } + + /// + /// All tree leaves (categories) within one tree should have same entity configuration id and same + /// set of attributes. + /// public Guid EntityConfigurationId { get; set; } + + /// + /// Tenant id - just a value for partitioning. + /// public Guid? TenantId { get; set; } } diff --git a/CloudFabric.EAV.Models/RequestModels/EntityInstanceCreateRequest.cs b/CloudFabric.EAV.Models/RequestModels/EntityInstanceCreateRequest.cs index 8c694a7..5f59ca1 100755 --- a/CloudFabric.EAV.Models/RequestModels/EntityInstanceCreateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/EntityInstanceCreateRequest.cs @@ -7,4 +7,7 @@ public class EntityInstanceCreateRequest public List Attributes { get; set; } public Guid? TenantId { get; set; } + public string? MachineName { get; set; } + + public List CategoryPaths { get; set; } } diff --git a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs index 1eec2ea..00d6477 100755 --- a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs +++ b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs @@ -9,8 +9,3 @@ public class EntityInstanceUpdateRequest public List AttributesToAddOrUpdate { get; set; } public List? AttributeMachineNamesToRemove { get; set; } } - -public class CategoryUpdateRequest: EntityInstanceUpdateRequest -{ - -} diff --git a/CloudFabric.EAV.Models/ViewModels/HierarchyViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryTreeViewModel.cs similarity index 85% rename from CloudFabric.EAV.Models/ViewModels/HierarchyViewModel.cs rename to CloudFabric.EAV.Models/ViewModels/CategoryTreeViewModel.cs index 77d7a0b..56dc122 100644 --- a/CloudFabric.EAV.Models/ViewModels/HierarchyViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryTreeViewModel.cs @@ -1,6 +1,6 @@ namespace CloudFabric.EAV.Models.ViewModels; -public class HierarchyViewModel +public class CategoryTreeViewModel { public Guid Id { get; set; } public string MachineName { get; protected set; } diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs index b224d60..dd42664 100644 --- a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs @@ -1,10 +1,10 @@ -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. -public class CategoryViewModel : EntityInstanceViewModel -{ - public string MachineName { get; set; } -} +// 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. +// public class CategoryViewModel : EntityInstanceViewModel +// { +// public string MachineName { get; set; } +// } diff --git a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs index da8f9f4..d559ecc 100755 --- a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs +++ b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs @@ -1,37 +1,29 @@ -using CloudFabric.EAV.Models.ViewModels.Attributes; +using System.Collections.ObjectModel; + +using CloudFabric.EAV.Models.ViewModels.Attributes; namespace CloudFabric.EAV.Models.ViewModels; -public class EntityInstanceViewModel +public record EntityInstanceViewModel { - public Guid Id { get; set; } + public Guid Id { get; init; } + + public Guid EntityConfigurationId { get; init; } - public Guid EntityConfigurationId { get; set; } + public string? MachineName { get; init; } - public List Attributes { get; set; } + public ReadOnlyCollection Attributes { get; init; } = + new ReadOnlyCollection(new List()); - public Guid? TenantId { get; set; } + public Guid? TenantId { get; init; } - public string PartitionKey { get; set; } + public string? PartitionKey { get; init; } - public List CategoryPaths { get; set; } + public ReadOnlyCollection CategoryPaths { get; init; } = + new ReadOnlyCollection(new List()); } -public class EntityTreeInstanceViewModel +public record EntityTreeInstanceViewModel : EntityInstanceViewModel { - public Guid Id { get; set; } - - public string MachineName { get; set; } - - public Guid EntityConfigurationId { get; set; } - - public List Attributes { get; set; } - - public Guid? TenantId { get; set; } - - public string PartitionKey { get; set; } - - public List CategoryPaths { get; set; } - - public List Children { get; set; } + public List Children { get; set; } = new (); } diff --git a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj index f7a353d..7319a43 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 4185c6e..7063f53 100644 --- a/CloudFabric.EAV.Service/EAVCategoryService.cs +++ b/CloudFabric.EAV.Service/EAVCategoryService.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using AutoMapper; @@ -7,63 +6,62 @@ 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 +public class EAVCategoryService { + private readonly ILogger _logger; + private readonly EventUserInfo _userInfo; + + internal readonly IMapper Mapper; + internal readonly EAVService EAVService; + + private readonly AggregateRepository _categoryTreeAggregateRepository; private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions; - public EAVCategoryService(ILogger> logger, + public EAVCategoryService( + ILogger logger, + EAVService eavService, IMapper mapper, - JsonSerializerOptions jsonSerializerOptions, AggregateRepositoryFactory aggregateRepositoryFactory, - ProjectionRepositoryFactory projectionRepositoryFactory, EventUserInfo userInfo, - ValueAttributeService valueAttributeService, - IOptions? elasticSearchQueryOptions = null) : base(logger, - new CategoryFromDictionaryDeserializer(mapper), - mapper, - jsonSerializerOptions, - aggregateRepositoryFactory, - projectionRepositoryFactory, - userInfo, - valueAttributeService) + IOptions? elasticSearchQueryOptions = null + ) { + _logger = logger; + _userInfo = userInfo; + Mapper = mapper; + + EAVService = eavService; + + _categoryTreeAggregateRepository = aggregateRepositoryFactory.GetAggregateRepository(); _elasticSearchQueryOptions = elasticSearchQueryOptions != null ? elasticSearchQueryOptions.Value : new ElasticSearchQueryOptions(); } - #region Categories - public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync( - CategoryTreeCreateRequest entity, + public async Task<(CategoryTreeViewModel, ProblemDetails)> CreateCategoryTreeAsync( + CategoryTreeCreateRequest categoryTreeCreateRequest, Guid? tenantId, CancellationToken cancellationToken = default ) { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( - entity.EntityConfigurationId, - entity.EntityConfigurationId.ToString(), - cancellationToken - ).ConfigureAwait(false); + EntityConfigurationViewModel? entityConfiguration = await EAVService.GetEntityConfiguration( + categoryTreeCreateRequest.EntityConfigurationId + ); if (entityConfiguration == null) { @@ -72,266 +70,79 @@ public EAVCategoryService(ILogger(tree), null)!; + _ = await _categoryTreeAggregateRepository.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 - ) + public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, + string entityInstancePartitionKey, Guid treeId, Guid? newParentId, CancellationToken cancellationToken = default) { - JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); - - return CreateCategoryInstance( - categoryJson.RootElement, - requestDeserializedCallback, - cancellationToken - ); - } + EntityInstance? entityInstance = await EAVService.EntityInstanceRepository + .LoadAsync(entityInstanceId, entityInstancePartitionKey, 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 - ); - } + if (entityInstance == null) + { + return (null, new ValidationErrorResponse(nameof(entityInstanceId), "Instance not found"))!; + } - /// - /// 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); + (var newCategoryPath, var parentId, ProblemDetails? errors) = + await BuildCategoryPath(treeId, newParentId, cancellationToken); - if (deserializationErrors != null) + if (errors != null) { - return (null, deserializationErrors); + return (null, errors)!; } - 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 - ); + entityInstance.UpdateCategoryPath(treeId, newCategoryPath ?? "", parentId!.Value); + var saved = await EAVService.EntityInstanceRepository + .SaveAsync(_userInfo, entityInstance, cancellationToken); - if (deserializationErrors != null) + if (!saved) { - return (null, deserializationErrors); + //TODO: What do we want to do with internal exceptions and unsuccessful flow? + throw new Exception("Entity was not saved"); } - if (requestDeserializedCallback != null) + return (Mapper.Map(entityInstance), null)!; + } + + internal async Task<(string?, Guid?, ProblemDetails?)> BuildCategoryPath(Guid treeId, Guid? parentId, + CancellationToken cancellationToken) + { + CategoryTree? tree = await _categoryTreeAggregateRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken); + if (tree == null) { - categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!); + return (null, null, new ValidationErrorResponse("TreeId", "Tree not found")); } - var (createdCategory, validationErrors) = await CreateCategoryInstance( - categoryInstanceCreateRequest!, cancellationToken - ); + EntityInstanceViewModel? parent = parentId == null + ? null + : await EAVService.GetEntityInstance( + parentId.GetValueOrDefault(), + tree.EntityConfigurationId.ToString() + ); - if (validationErrors != null) + if (parent == null && parentId != null) { - return (null, validationErrors); + return (null, null, new ValidationErrorResponse("ParentId", "Parent category not found")); } - return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null); + CategoryPathViewModel? parentPath = parent?.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId); + var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.MachineName}"; + return (categoryPath, parent?.Id, null); } - public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance( + public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateCategoryInstance( CategoryInstanceCreateRequest categoryCreateRequest, CancellationToken cancellationToken = default ) { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync( + CategoryTree? tree = await _categoryTreeAggregateRepository.LoadAsync( categoryCreateRequest.CategoryTreeId, categoryCreateRequest.CategoryTreeId.ToString(), cancellationToken @@ -350,25 +161,16 @@ public EAVCategoryService(ILogger attributeConfigurations = - await GetAttributeConfigurationsForEntityConfiguration( - entityConfiguration, - cancellationToken - ).ConfigureAwait(false); - - (var categoryPath, Guid? parentId, ProblemDetails? errors) = await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false); @@ -377,212 +179,35 @@ await GetAttributeConfigurationsForEntityConfiguration( 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))!; - } - - - - 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)) + var categoryTreeInstanceCreateRequest = new EntityInstanceCreateRequest { - machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString(); - if (machineName == null) + EntityConfigurationId = categoryCreateRequest.CategoryConfigurationId, + Attributes = categoryCreateRequest.Attributes, + MachineName = categoryCreateRequest.MachineName, + TenantId = categoryCreateRequest.TenantId, + CategoryPaths = new List() { - return (null, new ValidationErrorResponse("machineName", "Value is not a valid")); + new () { Path = categoryPath, ParentId = parentId, TreeId = tree.Id } } - } + }; - return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken); - } + var (categoryTreeInstanceCreated, problemDetails) = await EAVService + .CreateEntityInstance(categoryTreeInstanceCreateRequest, cancellationToken: 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) + if (problemDetails != null) { - return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; + return (null, problemDetails); } - List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration( - categoryConfiguration, - cancellationToken - ) - .ConfigureAwait(false); - - return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest( - categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson - ); + return (categoryTreeInstanceCreated, null); } - /// /// 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. + /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same level as a provided. /// /// /// /// - [SuppressMessage("Performance", "CA1806:Do not ignore method results")] public async Task> GetCategoryTreeViewAsync( Guid treeId, @@ -590,39 +215,37 @@ public async Task> GetCategoryTreeViewAsync( CancellationToken cancellationToken = default ) { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) - .ConfigureAwait(false); + CategoryTree? tree = await _categoryTreeAggregateRepository + .LoadAsync(treeId, treeId.ToString(), cancellationToken); + if (tree == null) { throw new NotFoundException("Category tree not found"); } - ProjectionQueryResult treeElementsQueryResult = - await QueryInstances(tree.EntityConfigurationId, + ProjectionQueryResult treeElementsQueryResult = + await EAVService.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!) - .Select(x => - { - x.CategoryPaths = x.CategoryPaths.Where(cp => cp.TreeId == treeId).ToList(); - return x; - }).ToList(); - + .Select(x => x with + { + CategoryPaths = x.CategoryPaths.Where(cp => cp.TreeId == treeId).ToList().AsReadOnly() + } + ).ToList(); return BuildTreeView(treeElements, notDeeperThanCategoryId); - } - private List BuildTreeView(List categories, Guid? notDeeperThanCategoryId) + private List BuildTreeView(List categories, Guid? notDeeperThanCategoryId) { - int searchedLevelPathLenght; if (notDeeperThanCategoryId != null) @@ -637,16 +260,18 @@ private List BuildTreeView(List searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault()!.Path.Length; categories = categories - .Where(x => x.CategoryPaths.FirstOrDefault()!.Path.Length <= searchedLevelPathLenght).ToList(); + .Where(x => x.CategoryPaths.FirstOrDefault()! + .Path.Length <= searchedLevelPathLenght) + .ToList(); } var treeViewModel = new List(); // Go through each instance once - foreach (CategoryViewModel treeElement in categories + foreach (EntityInstanceViewModel treeElement in categories .OrderBy(x => x.CategoryPaths.FirstOrDefault()?.Path.Length)) { - var treeElementViewModel = _mapper.Map(treeElement); + var treeElementViewModel = Mapper.Map(treeElement); var categoryPath = treeElement.CategoryPaths.FirstOrDefault()?.Path; // If categoryPath is empty, that this is a root model -> add it directly to the tree @@ -676,8 +301,8 @@ private List BuildTreeView(List // If it is not still there -> find it in the global list of categories and add to our treeViewModel if (parent == null) { - CategoryViewModel? parentInstance = categories.FirstOrDefault(y => y.MachineName == pathComponent); - parent = _mapper.Map(parentInstance); + EntityInstanceViewModel? parentInstance = categories.FirstOrDefault(y => y.MachineName == pathComponent); + parent = Mapper.Map(parentInstance); treeViewModelCurrent.Add(parent); } @@ -689,24 +314,23 @@ private List BuildTreeView(List currentLevel?.Children.Add(treeElementViewModel); } } - return treeViewModel; + return treeViewModel; } - /// /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format. /// /// /// /// - public async Task> GetSubcategories( + public async Task> GetSubcategories( Guid categoryTreeId, Guid? parentId = null, string? parentMachineName = null, CancellationToken cancellationToken = default ) { - var categoryTree = await _categoryTreeRepository.LoadAsync( + var categoryTree = await _categoryTreeAggregateRepository.LoadAsync( categoryTreeId, categoryTreeId.ToString(), cancellationToken ).ConfigureAwait(false); @@ -717,11 +341,11 @@ private List BuildTreeView(List var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken); - var queryResult = _mapper.Map>( - await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) + var queryResult = Mapper.Map>( + await EAVService.QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken) ); - return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); + return queryResult.Records.Select(x => x.Document).ToList() ?? new List(); } private ProjectionQuery GetSubcategoriesPrepareQuery( @@ -738,7 +362,7 @@ private ProjectionQuery GetSubcategoriesPrepareQuery( query.Filters.Add(new Filter { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", + PropertyName = $"{nameof(EntityInstanceViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}", Operator = FilterOperator.Equal, Value = categoryTree.Id.ToString(), }); @@ -748,9 +372,9 @@ private ProjectionQuery GetSubcategoriesPrepareQuery( { query.Filters.Add(new Filter { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + PropertyName = $"{nameof(EntityInstanceViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", Operator = FilterOperator.Equal, - Value = string.Empty, + Value = null, }); return query; } @@ -760,7 +384,7 @@ private ProjectionQuery GetSubcategoriesPrepareQuery( query.Filters.Add(new Filter { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", + PropertyName = $"{nameof(EntityInstanceViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}", Operator = FilterOperator.Equal, Value = parentId.ToString() }); @@ -771,7 +395,7 @@ private ProjectionQuery GetSubcategoriesPrepareQuery( query.Filters.Add(new Filter { - PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", + PropertyName = $"{nameof(EntityInstanceViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}", Operator = FilterOperator.Equal, Value = parentMachineName }); diff --git a/CloudFabric.EAV.Service/EAVCategoryServiceJsonExtensions.cs b/CloudFabric.EAV.Service/EAVCategoryServiceJsonExtensions.cs new file mode 100644 index 0000000..864667e --- /dev/null +++ b/CloudFabric.EAV.Service/EAVCategoryServiceJsonExtensions.cs @@ -0,0 +1,430 @@ +using System.Text.Json; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Service.Serialization; + +using Microsoft.AspNetCore.Mvc; + +namespace CloudFabric.EAV.Service; + +public static class EAVCategoryServiceJsonExtensions +{ + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "description": "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 static Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + this EAVCategoryService eavCategoryService, + string categoryJsonString, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return eavCategoryService.CreateCategoryInstance( + categoryJson.RootElement, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json string. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "description": "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 static Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + this EAVCategoryService eavCategoryService, + string categoryJsonString, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString); + + return eavCategoryService.CreateCategoryInstance( + categoryJson.RootElement, + machineName, + categoryConfigurationId, + categoryTreeId, + parentId, + tenantId, + requestDeserializedCallback, + cancellationToken + ); + } + + /// + /// Create new category from provided json document. + /// + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "description": "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 static async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + this EAVCategoryService eavCategoryService, + JsonElement categoryJson, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + var (categoryInstanceCreateRequest, deserializationErrors) = + await eavCategoryService.DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await eavCategoryService.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", + /// "description": "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 static async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance( + this EAVCategoryService eavCategoryService, + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + Func>? requestDeserializedCallback = null, + CancellationToken cancellationToken = default + ) + { + (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors) + = await eavCategoryService.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 eavCategoryService + .CreateCategoryInstance( + categoryInstanceCreateRequest!, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (eavCategoryService.EAVService.SerializeEntityInstanceToJsonMultiLanguage( + eavCategoryService.Mapper.Map(createdCategory)), null + ); + } + + + /// + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "description": "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 static async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + this EAVCategoryService eavCategoryService, + 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 eavCategoryService.DeserializeCategoryInstanceCreateRequestFromJson( + categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken + ); + } + + /// Use following json format: + /// + /// ``` + /// { + /// "name": "Main Category", + /// "description": "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 static async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson( + this EAVCategoryService eavCategoryService, + JsonElement categoryJson, + string machineName, + Guid categoryConfigurationId, + Guid categoryTreeId, + Guid? parentId, + Guid? tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? categoryConfiguration = await eavCategoryService.EAVService.EntityConfigurationRepository + .LoadAsync( + categoryConfigurationId, + categoryConfigurationId.ToString(), + cancellationToken + ); + + if (categoryConfiguration == null) + { + return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!; + } + + List attributeConfigurations = await eavCategoryService.EAVService + .GetAttributeConfigurationsForEntityConfiguration( + categoryConfiguration, + cancellationToken + ); + + var deserializer = new EntityInstanceCreateUpdateRequestFromJsonDeserializer( + eavCategoryService.EAVService.AttributeConfigurationRepository, + eavCategoryService.EAVService.JsonSerializerOptions + ); + + return await deserializer.DeserializeCategoryInstanceCreateRequest( + categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, + categoryJson + ); + } +} diff --git a/CloudFabric.EAV.Service/EAVEntityInstanceService.cs b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs index e1ecd58..215097c 100644 --- a/CloudFabric.EAV.Service/EAVEntityInstanceService.cs +++ b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs @@ -1,479 +1,42 @@ -using System.Text.Json; - -using AutoMapper; - -using CloudFabric.EAV.Domain.GeneratedValues; -using CloudFabric.EAV.Domain.Models; -using CloudFabric.EAV.Enums; -using CloudFabric.EAV.Models.RequestModels; -using CloudFabric.EAV.Models.ViewModels; -using CloudFabric.EAV.Service.Serialization; -using CloudFabric.EventSourcing.Domain; -using CloudFabric.EventSourcing.EventStore.Persistence; -using CloudFabric.Projections; - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -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, - ValueAttributeService valueAttributeService) : base(logger, - new EntityInstanceFromDictionaryDeserializer(mapper), - mapper, - jsonSerializerOptions, - aggregateRepositoryFactory, - projectionRepositoryFactory, - userInfo, - valueAttributeService) - { - } - - /// - /// 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(); - List generatedValues = new(); - - 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()); - } - - generatedValues.Add(await _valueAttributeService.GenerateAttributeInstanceValue(entityConfiguration, a, attributeValue)); - } - - if (validationErrors.Count > 0) - { - return (null, new ValidationErrorResponse(validationErrors))!; - } - - if (!dryRun) - { - var response = await _valueAttributeService.SaveValues(entityConfiguration.Id, generatedValues); - - foreach (var actionResponse in response.Where(x => x.Status == GeneratedValueActionStatus.Failed)) - { - var attributeMachineName = attributeConfigurations.First(x => x.Id == actionResponse.AttributeConfigurationId).MachineName; - - validationErrors.Add( - attributeMachineName, - new string[] { $"Failed to generate value: {actionResponse.GeneratedValueType?.Name}" }); - } - - 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); - } -} +// using System.Text.Json; +// +// using AutoMapper; +// +// using CloudFabric.EAV.Domain.GeneratedValues; +// using CloudFabric.EAV.Domain.Models; +// using CloudFabric.EAV.Enums; +// using CloudFabric.EAV.Models.RequestModels; +// using CloudFabric.EAV.Models.ViewModels; +// using CloudFabric.EAV.Service.Serialization; +// using CloudFabric.EventSourcing.Domain; +// using CloudFabric.EventSourcing.EventStore.Persistence; +// using CloudFabric.Projections; +// +// using Microsoft.AspNetCore.Mvc; +// using Microsoft.Extensions.Logging; +// +// 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, +// ValueAttributeService valueAttributeService) : base(logger, +// new EntityInstanceFromDictionaryDeserializer(mapper), +// mapper, +// jsonSerializerOptions, +// aggregateRepositoryFactory, +// projectionRepositoryFactory, +// userInfo, +// valueAttributeService) +// { +// } +// +// } diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs index 392a541..08c4ee7 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; @@ -31,41 +30,36 @@ namespace CloudFabric.EAV.Service; -[SuppressMessage("ReSharper", "InconsistentNaming")] -public abstract class EAVService where TViewModel : EntityInstanceViewModel - where TUpdateRequest : EntityInstanceUpdateRequest - where TEntityType : EntityInstanceBase +public class EAVService { - private readonly IProjectionRepository - _attributeConfigurationProjectionRepository; + private readonly ILogger _logger; + private readonly IMapper _mapper; + internal readonly JsonSerializerOptions JsonSerializerOptions; + private readonly EventUserInfo _userInfo; + + internal readonly AggregateRepository EntityConfigurationRepository; + internal readonly AggregateRepository EntityInstanceRepository; - private readonly AggregateRepository _attributeConfigurationRepository; + internal readonly AggregateRepository AttributeConfigurationRepository; + internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory; + + private readonly IProjectionRepository + _attributeConfigurationProjectionRepository; private readonly IProjectionRepository _entityConfigurationProjectionRepository; - internal readonly AggregateRepository _entityConfigurationRepository; - private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer; - internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer + private readonly InstanceFromDictionaryDeserializer + _entityInstanceFromDictionaryDeserializer; + private readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer _entityInstanceCreateUpdateRequestFromJsonDeserializer; - internal readonly AggregateRepository _entityInstanceRepository; - private readonly ILogger> _logger; - internal readonly IMapper _mapper; - private readonly JsonSerializerOptions _jsonSerializerOptions; - internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory; - - internal readonly EventUserInfo _userInfo; - - internal readonly AggregateRepository _categoryTreeRepository; - internal readonly AggregateRepository _categoryInstanceRepository; internal readonly ValueAttributeService _valueAttributeService; - protected EAVService( - ILogger> logger, - InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer, + public EAVService( + ILogger logger, IMapper mapper, JsonSerializerOptions jsonSerializerOptions, AggregateRepositoryFactory aggregateRepositoryFactory, @@ -75,38 +69,31 @@ protected EAVService( { _logger = logger; _mapper = mapper; - _jsonSerializerOptions = jsonSerializerOptions; + JsonSerializerOptions = jsonSerializerOptions; _projectionRepositoryFactory = projectionRepositoryFactory; _userInfo = userInfo; - - - _attributeConfigurationRepository = aggregateRepositoryFactory + AttributeConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityConfigurationRepository = aggregateRepositoryFactory + EntityConfigurationRepository = aggregateRepositoryFactory .GetAggregateRepository(); - _entityInstanceRepository = aggregateRepositoryFactory - .GetAggregateRepository(); - + EntityInstanceRepository = aggregateRepositoryFactory + .GetAggregateRepository(); _attributeConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); _entityConfigurationProjectionRepository = _projectionRepositoryFactory .GetProjectionRepository(); - _entityInstanceFromDictionaryDeserializer = instanceFromDictionaryDeserializer; + _entityInstanceFromDictionaryDeserializer = new InstanceFromDictionaryDeserializer(_mapper); _entityInstanceCreateUpdateRequestFromJsonDeserializer = new EntityInstanceCreateUpdateRequestFromJsonDeserializer( - _attributeConfigurationRepository, jsonSerializerOptions + AttributeConfigurationRepository, jsonSerializerOptions ); - _categoryInstanceRepository = aggregateRepositoryFactory - .GetAggregateRepository(); - _categoryTreeRepository = aggregateRepositoryFactory - .GetAggregateRepository(); _valueAttributeService = valueAttributeService; } @@ -218,38 +205,11 @@ private async Task BuildCategoryPath(Guid treeId, Guid? parentId, - CancellationToken cancellationToken) - { - CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken) - .ConfigureAwait(false); - if (tree == null) - { - return (null, null, new ValidationErrorResponse("TreeId", "Tree not found")); - } - - Category? parent = parentId == null - ? null - : _mapper.Map(await _categoryInstanceRepository - .LoadAsync(parentId.Value, tree.EntityConfigurationId.ToString(), cancellationToken) - .ConfigureAwait(false) - ); - - if (parent == null && parentId != null) - { - return (null, null, new ValidationErrorResponse("ParentId", "Parent category not found")); - } - - CategoryPath? parentPath = parent?.CategoryPaths.FirstOrDefault(x => x.TreeId == treeId); - var categoryPath = parentPath == null ? "" : $"{parentPath.Path}/{parent?.MachineName}"; - return (categoryPath, parent?.Id, null); - } - #region EntityConfiguration public async Task GetEntityConfiguration(Guid id) { - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync(id, id.ToString()); + EntityConfiguration? entityConfiguration = await EntityConfigurationRepository.LoadAsync(id, id.ToString()); return _mapper.Map(entityConfiguration); } @@ -259,7 +219,7 @@ public async Task GetEntityConfigura CancellationToken cancellationToken = default ) { - var entityConfiguration = await _entityConfigurationRepository.LoadAsyncOrThrowNotFound( + var entityConfiguration = await EntityConfigurationRepository.LoadAsyncOrThrowNotFound( id, id.ToString(), // EntityConfiguration partition key is set to it's id in Domain model cancellationToken @@ -333,7 +293,7 @@ await CreateArrayElementConfiguration(array.ItemsType, } - await _attributeConfigurationRepository.SaveAsync(_userInfo, attribute, cancellationToken) + await AttributeConfigurationRepository.SaveAsync(_userInfo, attribute, cancellationToken) .ConfigureAwait(false); return (_mapper.Map(attribute), null); @@ -346,7 +306,7 @@ public async Task GetAttribute( CancellationToken cancellationToken = default ) { - AttributeConfiguration attribute = await _attributeConfigurationRepository + AttributeConfiguration attribute = await AttributeConfigurationRepository .LoadAsyncOrThrowNotFound(id, id.ToString(), cancellationToken); if (attribute.IsDeleted) @@ -361,7 +321,7 @@ public async Task GetAttribute( Guid id, AttributeConfigurationCreateUpdateRequest updateRequest, CancellationToken cancellationToken = default) { - AttributeConfiguration? attribute = await _attributeConfigurationRepository + AttributeConfiguration? attribute = await AttributeConfigurationRepository .LoadAsync(id, id.ToString(), cancellationToken); if (attribute == null || attribute.IsDeleted) @@ -388,7 +348,7 @@ public async Task GetAttribute( await _attributeConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - await _attributeConfigurationRepository.SaveAsync(_userInfo, attribute, cancellationToken) + await AttributeConfigurationRepository.SaveAsync(_userInfo, attribute, cancellationToken) .ConfigureAwait(false); return (_mapper.Map(attribute), null); @@ -432,7 +392,7 @@ CancellationToken cancellationToken { var requestAttribute = (EntityAttributeConfigurationCreateUpdateReferenceRequest)attribute; - AttributeConfiguration attributeConfiguration = await _attributeConfigurationRepository + AttributeConfiguration attributeConfiguration = await AttributeConfigurationRepository .LoadAsyncOrThrowNotFound( requestAttribute.AttributeConfigurationId, requestAttribute.AttributeConfigurationId.ToString(), @@ -511,7 +471,7 @@ await CreateAttribute( await _valueAttributeService.InitializeEntityConfigurationGeneratedValues(entityConfiguration.Id, allCreatedAttributes); - await _entityConfigurationRepository.SaveAsync( + await EntityConfigurationRepository.SaveAsync( _userInfo, entityConfiguration, cancellationToken @@ -546,7 +506,7 @@ await _entityConfigurationRepository.SaveAsync( ); } - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + EntityConfiguration? entityConfiguration = await EntityConfigurationRepository.LoadAsync( entityUpdateRequest.Id, entityUpdateRequest.Id.ToString(), cancellationToken @@ -583,7 +543,7 @@ await _entityConfigurationRepository.SaveAsync( var attributeShouldBeAdded = entityConfiguration.Attributes .All(a => a.AttributeConfigurationId != attributeReferenceUpdate.AttributeConfigurationId); - AttributeConfiguration? attributeConfiguration = await _attributeConfigurationRepository.LoadAsync( + AttributeConfiguration? attributeConfiguration = await AttributeConfigurationRepository.LoadAsync( attributeReferenceUpdate.AttributeConfigurationId, attributeReferenceUpdate.AttributeConfigurationId.ToString(), cancellationToken @@ -672,7 +632,7 @@ await CreateAttribute( } //await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) + await EntityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); await EnsureProjectionIndexForEntityConfiguration(entityConfiguration); @@ -699,7 +659,7 @@ private async Task EnsureProjectionIndexForEntityConfiguration(EntityConfigurati CancellationToken cancellationToken = default ) { - AttributeConfiguration attributeConfiguration = await _attributeConfigurationRepository + AttributeConfiguration attributeConfiguration = await AttributeConfigurationRepository .LoadAsyncOrThrowNotFound( attributeId, attributeId.ToString(), @@ -711,7 +671,7 @@ private async Task EnsureProjectionIndexForEntityConfiguration(EntityConfigurati return (null, new ValidationErrorResponse(nameof(attributeId), "Attribute not found")); } - EntityConfiguration entityConfiguration = await _entityConfigurationRepository.LoadAsyncOrThrowNotFound( + EntityConfiguration entityConfiguration = await EntityConfigurationRepository.LoadAsyncOrThrowNotFound( entityConfigurationId, entityConfigurationId.ToString(), cancellationToken @@ -736,7 +696,7 @@ private async Task EnsureProjectionIndexForEntityConfiguration(EntityConfigurati entityConfiguration.AddAttribute(attributeId); await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) + await EntityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken) .ConfigureAwait(false); await _valueAttributeService.InitializeGeneratedValue(entityConfigurationId, attributeConfiguration); @@ -752,7 +712,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c { EnsureAttributeMachineNameIsAdded(attributeConfigurationCreateUpdateRequest); - EntityConfiguration entityConfiguration = await _entityConfigurationRepository.LoadAsyncOrThrowNotFound( + EntityConfiguration entityConfiguration = await EntityConfigurationRepository.LoadAsyncOrThrowNotFound( entityConfigurationId, entityConfigurationId.ToString(), cancellationToken @@ -784,7 +744,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c entityConfiguration.AddAttribute(createdAttribute.Id); await _attributeConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken); + await EntityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken); await _valueAttributeService.InitializeGeneratedValue(entityConfigurationId, createdAttribute); @@ -794,7 +754,7 @@ await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, c public async Task DeleteAttributesFromEntityConfiguration(List attributesIds, Guid entityConfigurationId, CancellationToken cancellationToken = default) { - EntityConfiguration entityConfiguration = await _entityConfigurationRepository.LoadAsyncOrThrowNotFound( + EntityConfiguration entityConfiguration = await EntityConfigurationRepository.LoadAsyncOrThrowNotFound( entityConfigurationId, entityConfigurationId.ToString(), cancellationToken @@ -813,14 +773,14 @@ public async Task DeleteAttributesFromEntityConfiguration(List attributesI await _entityConfigurationProjectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false); - await _entityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken); + await EntityConfigurationRepository.SaveAsync(_userInfo, entityConfiguration, cancellationToken); } public async Task DeleteAttributes(List attributesIds, CancellationToken cancellationToken = default) { foreach (Guid attributeId in attributesIds) { - AttributeConfiguration? attributeConfiguration = await _attributeConfigurationRepository.LoadAsync( + AttributeConfiguration? attributeConfiguration = await AttributeConfigurationRepository.LoadAsync( attributeId, attributeId.ToString(), cancellationToken @@ -833,7 +793,7 @@ public async Task DeleteAttributes(List attributesIds, CancellationToken c { attributeConfiguration.Delete(); - await _attributeConfigurationRepository + await AttributeConfigurationRepository .SaveAsync(_userInfo, attributeConfiguration, cancellationToken).ConfigureAwait(false); } } @@ -892,7 +852,7 @@ await _entityConfigurationProjectionRepository.Query( var defaultTypeConfig = DefaultAttributeConfigurationFactory.GetDefaultConfiguration(type, $"element_config_{machineName}", tenantId); if (defaultTypeConfig != null) { - var saved = await _attributeConfigurationRepository.SaveAsync(_userInfo, defaultTypeConfig, cancellationToken).ConfigureAwait(false); + var saved = await AttributeConfigurationRepository.SaveAsync(_userInfo, defaultTypeConfig, cancellationToken).ConfigureAwait(false); resultGuid = saved ? defaultTypeConfig.Id : null; } } @@ -943,96 +903,144 @@ private void FillMissedValuesInConfiguration(AttributeConfigurationCreateUpdateR }; } } - #endregion - // public async Task> ListEntityInstances(string entityConfigurationMachineName, int take, int skip = 0) - // { - // var records = await _entityInstanceRepository - // .GetQuery() - // .Where(e => e.EntityConfiguration.MachineName == entityConfigurationMachineName) - // .Take(take) - // .Skip(skip) - // .ToListAsync(); - // - // return _mapper.Map>(records); - // } - // - // public async Task> ListEntityInstances(Guid entityConfigurationId, int take, int skip = 0) - // { - // var records = await _entityInstanceRepository - // .GetQuery() - // .Where(e => e.EntityConfigurationId == entityConfigurationId) - // .Take(take) - // .Skip(skip) - // .ToListAsync(); - // - // return _mapper.Map>(records); - // } - - #region BaseEntityInstance - - public async Task GetEntityInstance(Guid id, string partitionKey) + internal async Task> GetAttributeConfigurationsForEntityConfiguration( + EntityConfiguration entityConfiguration, CancellationToken cancellationToken = default + ) { - TEntityType? entityInstance = await _entityInstanceRepository.LoadAsync(id, partitionKey); + var attributeConfigurations = new List(); - return _mapper.Map(entityInstance); + foreach (EntityConfigurationAttributeReference attributeReference in entityConfiguration.Attributes) + { + attributeConfigurations.Add( + await AttributeConfigurationRepository.LoadAsyncOrThrowNotFound( + attributeReference.AttributeConfigurationId, + attributeReference.AttributeConfigurationId.ToString(), + cancellationToken + ) + ); + } + + return attributeConfigurations; } + #endregion + + #region EntityInstance - public async Task GetEntityInstanceJsonMultiLanguage(Guid id, string partitionKey) + public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance( + EntityInstanceCreateRequest entity, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) { - TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + EntityConfiguration? entityConfiguration = await EntityConfigurationRepository.LoadAsync( + entity.EntityConfigurationId, + entity.EntityConfigurationId.ToString(), + cancellationToken + ); - return SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); - } + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!; + } - public async Task GetEntityInstanceJsonSingleLanguage( - Guid id, - string partitionKey, - string language, - string fallbackLanguage = "en-US") - { - TViewModel? entityInstanceViewModel = await GetEntityInstance(id, partitionKey); + List attributeConfigurations = + await GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ); - return SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); - } + //TODO: add check for categoryPath + var entityInstance = new EntityInstance( + Guid.NewGuid(), + entity.EntityConfigurationId, + _mapper.Map>(entity.Attributes), + entity.MachineName, + entity.TenantId, + _mapper.Map>(entity.CategoryPaths) + ); - public JsonDocument SerializeEntityInstanceToJsonMultiLanguage(TViewModel? entityInstanceViewModel) - { - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + var validationErrors = new Dictionary(); + List generatedValues = new(); + + 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()); + } + + generatedValues.Add(await _valueAttributeService.GenerateAttributeInstanceValue(entityConfiguration, a, attributeValue)); + } + + if (validationErrors.Count > 0) + { + return (null, new ValidationErrorResponse(validationErrors))!; + } + + if (!dryRun) + { + var response = await _valueAttributeService.SaveValues(entityConfiguration.Id, generatedValues); + + foreach (var actionResponse in response.Where(x => x.Status == GeneratedValueActionStatus.Failed)) + { + var attributeMachineName = attributeConfigurations.First(x => x.Id == actionResponse.AttributeConfigurationId).MachineName; + + validationErrors.Add( + attributeMachineName, + new string[] { $"Failed to generate value: {actionResponse.GeneratedValueType?.Name}" }); + } + + ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory + .FromEntityConfiguration(entityConfiguration, attributeConfigurations); - return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); + 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); } - public JsonDocument SerializeEntityInstanceToJsonSingleLanguage( - TViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" - ) + public async Task GetEntityInstance(Guid id, string partitionKey) { - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + EntityInstance? entityInstance = await EntityInstanceRepository.LoadAsync(id, partitionKey); - return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); + return _mapper.Map(entityInstance); } - public async Task<(TViewModel, ProblemDetails)> UpdateEntityInstance( + public async Task<(EntityInstanceViewModel, ProblemDetails)> UpdateEntityInstance( string partitionKey, - TUpdateRequest updateRequest, + EntityInstanceUpdateRequest updateRequest, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default ) { - TEntityType? entityInstance = - await _entityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); + EntityInstance? entityInstance = + await EntityInstanceRepository.LoadAsync(updateRequest.Id, partitionKey, cancellationToken); if (entityInstance == null) { return (null, new ValidationErrorResponse(nameof(updateRequest.Id), "Entity instance not found"))!; } - EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync( + EntityConfiguration? entityConfiguration = await EntityConfigurationRepository.LoadAsync( entityInstance.EntityConfigurationId, entityInstance.EntityConfigurationId.ToString(), cancellationToken @@ -1147,7 +1155,7 @@ await GetAttributeConfigurationsForEntityConfiguration( { await _valueAttributeService.SaveValues(entityConfiguration.Id, generatedValues); - var entityInstanceSaved = await _entityInstanceRepository + var entityInstanceSaved = await EntityInstanceRepository .SaveAsync(_userInfo, entityInstance, cancellationToken) .ConfigureAwait(false); if (!entityInstanceSaved) @@ -1156,7 +1164,7 @@ await GetAttributeConfigurationsForEntityConfiguration( } } - return (_mapper.Map(entityInstance), null)!; + return (_mapper.Map(entityInstance), null)!; } /// @@ -1168,13 +1176,13 @@ await GetAttributeConfigurationsForEntityConfiguration( /// /// /// - public async Task> QueryInstances( + public async Task> QueryInstances( Guid entityConfigurationId, ProjectionQuery query, CancellationToken cancellationToken = default ) { - EntityConfiguration entityConfiguration = await _entityConfigurationRepository.LoadAsyncOrThrowNotFound( + EntityConfiguration entityConfiguration = await EntityConfigurationRepository.LoadAsyncOrThrowNotFound( entityConfigurationId, entityConfigurationId.ToString(), cancellationToken @@ -1199,149 +1207,5 @@ public async Task> QueryInstances( ); } - /// - /// Returns records in json serialized format. - /// LocalizedStrings are returned as objects whose property names are language identifiers - /// and property values are language translation strings. - /// - /// EntityInstance with: - /// - /// - one text attribute of type LocalizedString "productName" - /// - one number attribute of type Number "price" - /// - /// will be returned in following json format: - /// - /// ``` - /// { - /// "productName": { - /// "en-US": "Terraforming Mars", - /// "ru-RU": "Покорение Марса" - /// }, - /// "price": 100 - /// } - /// ``` - /// - /// - /// - /// - /// - public async Task> QueryInstancesJsonMultiLanguage( - Guid entityConfigurationId, - ProjectionQuery query, - CancellationToken cancellationToken = default - ) - { - var results = await QueryInstances( - entityConfigurationId, - query, - cancellationToken - ); - - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); - - return results.TransformResultDocuments( - r => JsonSerializer.SerializeToDocument(r, serializerOptions) - ); - } - - /// - /// Returns records in json serialized format. - /// LocalizedStrings are converted to a single language string of the language passed in parameters. - /// - /// EntityInstance with: - /// - /// - one text attribute of type LocalizedString "productName" - /// - one number attribute of type Number "price" - /// - /// will be returned in following json format: - /// - /// ``` - /// { - /// "productName": "Terraforming Mars", - /// "price": 100 - /// } - /// ``` - /// - /// - /// - /// Language to use from all localized strings. Only this language strings will be returned. - /// If main language will not be found, this language will be tried. Defaults to en-US. - /// - /// - public async Task> QueryInstancesJsonSingleLanguage( - Guid entityConfigurationId, - ProjectionQuery query, - string language = "en-US", - string fallbackLanguage = "en-US", - CancellationToken cancellationToken = default - ) - { - var results = await QueryInstances( - entityConfigurationId, - query, - cancellationToken - ); - - var serializerOptions = new JsonSerializerOptions(_jsonSerializerOptions); - serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); - serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); - - return results.TransformResultDocuments( - r => JsonSerializer.SerializeToDocument(r, serializerOptions) - ); - } - - public async Task<(TViewModel, ProblemDetails)> UpdateCategoryPath(Guid entityInstanceId, - string entityInstancePartitionKey, Guid treeId, Guid? newParentId, CancellationToken cancellationToken = default) - { - TEntityType? entityInstance = await _entityInstanceRepository - .LoadAsync(entityInstanceId, entityInstancePartitionKey, cancellationToken).ConfigureAwait(false); - if (entityInstance == null) - { - return (null, new ValidationErrorResponse(nameof(entityInstanceId), "Instance not found"))!; - } - - (var newCategoryPath, var parentId, ProblemDetails? errors) = - await BuildCategoryPath(treeId, newParentId, cancellationToken).ConfigureAwait(false); - - if (errors != null) - { - return (null, errors)!; - } - - entityInstance.ChangeCategoryPath(treeId, newCategoryPath ?? "", parentId!.Value); - var saved = await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, 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(entityInstance), null)!; - } - - internal async Task> GetAttributeConfigurationsForEntityConfiguration( - EntityConfiguration entityConfiguration, CancellationToken cancellationToken = default - ) - { - var attributeConfigurations = new List(); - - foreach (EntityConfigurationAttributeReference attributeReference in entityConfiguration.Attributes) - { - attributeConfigurations.Add( - await _attributeConfigurationRepository.LoadAsyncOrThrowNotFound( - attributeReference.AttributeConfigurationId, - attributeReference.AttributeConfigurationId.ToString(), - cancellationToken - ) - ); - } - - return attributeConfigurations; - } - #endregion } diff --git a/CloudFabric.EAV.Service/EAVServiceJsonExtensions.cs b/CloudFabric.EAV.Service/EAVServiceJsonExtensions.cs new file mode 100644 index 0000000..c7a8182 --- /dev/null +++ b/CloudFabric.EAV.Service/EAVServiceJsonExtensions.cs @@ -0,0 +1,520 @@ +using System.Text.Json; + +using CloudFabric.EAV.Domain.Models; +using CloudFabric.EAV.Models.RequestModels; +using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Service.Serialization; +using CloudFabric.Projections.Queries; + +using Microsoft.AspNetCore.Mvc; + +namespace CloudFabric.EAV.Service; + +public static class EAVServiceJsonExtensions +{ + /// + /// 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 + /// Well, sometimes it's needed to just import the data and then + /// fill out missing values. + /// + /// + public static Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + this EAVService eavService, + string entityJsonString, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return eavService.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 + /// Well, sometimes it's needed to just import the data and then + /// fill out missing values. + /// + /// + public static Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + this EAVService eavService, + string entityJsonString, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + JsonDocument entityJson = JsonDocument.Parse(entityJsonString); + + return eavService.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 + /// Well, sometimes it's needed to just import the data and then + /// fill out missing values. + /// + /// + public static async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + this EAVService eavService, + JsonElement entityJson, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = + await eavService.DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + return await eavService.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 + /// Well, sometimes it's needed to just import the data and then + /// fill out missing values. + /// + /// + public static async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance( + this EAVService eavService, + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + Func>? requestDeserializedCallback = null, + bool dryRun = false, + bool requiredAttributesCanBeNull = false, + CancellationToken cancellationToken = default + ) + { + var (entityInstanceCreateRequest, deserializationErrors) = await + eavService.DeserializeEntityInstanceCreateRequestFromJson( + entityJson, entityConfigurationId, tenantId, cancellationToken + ); + + if (deserializationErrors != null) + { + return (null, deserializationErrors); + } + + if (requestDeserializedCallback != null) + { + entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun); + } + + var (createdEntity, validationErrors) = await eavService.CreateEntityInstance( + entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken + ); + + if (validationErrors != null) + { + return (null, validationErrors); + } + + return (eavService.SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null); + } + + + public static async Task GetEntityInstanceJsonMultiLanguage( + this EAVService eavService, Guid id, string partitionKey) + { + EntityInstanceViewModel? entityInstanceViewModel = await eavService.GetEntityInstance(id, partitionKey); + + return eavService.SerializeEntityInstanceToJsonMultiLanguage(entityInstanceViewModel); + } + + public static async Task GetEntityInstanceJsonSingleLanguage( + this EAVService eavService, + Guid id, + string partitionKey, + string language, + string fallbackLanguage = "en-US") + { + EntityInstanceViewModel? entityInstanceViewModel = await eavService.GetEntityInstance(id, partitionKey); + + return eavService.SerializeEntityInstanceToJsonSingleLanguage(entityInstanceViewModel, language, fallbackLanguage); + } + + public static JsonDocument SerializeEntityInstanceToJsonMultiLanguage( + this EAVService eavService, EntityInstanceViewModel? entityInstanceViewModel) + { + var serializerOptions = new JsonSerializerOptions(eavService.JsonSerializerOptions); + serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + + return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); + } + + public static JsonDocument SerializeEntityInstanceToJsonSingleLanguage( + this EAVService eavService, + EntityInstanceViewModel? entityInstanceViewModel, string language, string fallbackLanguage = "en-US" + ) + { + var serializerOptions = new JsonSerializerOptions(eavService.JsonSerializerOptions); + serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + + return JsonSerializer.SerializeToDocument(entityInstanceViewModel, serializerOptions); + } + + /// + /// 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 static async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + this EAVService eavService, + 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 eavService.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 static async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson( + this EAVService eavService, + JsonElement entityJson, + Guid entityConfigurationId, + Guid tenantId, + CancellationToken cancellationToken = default + ) + { + EntityConfiguration? entityConfiguration = await eavService.EntityConfigurationRepository.LoadAsync( + entityConfigurationId, + entityConfigurationId.ToString(), + cancellationToken + ) + .ConfigureAwait(false); + + if (entityConfiguration == null) + { + return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!; + } + + List attributeConfigurations = + await eavService.GetAttributeConfigurationsForEntityConfiguration( + entityConfiguration, + cancellationToken + ) + .ConfigureAwait(false); + + var deserializer = new EntityInstanceCreateUpdateRequestFromJsonDeserializer( + eavService.AttributeConfigurationRepository, eavService.JsonSerializerOptions + ); + + return await deserializer.DeserializeEntityInstanceCreateRequest( + entityConfigurationId, tenantId, attributeConfigurations, entityJson + ); + } + + /// + /// Returns records in json serialized format. + /// LocalizedStrings are returned as objects whose property names are language identifiers + /// and property values are language translation strings. + /// + /// EntityInstance with: + /// + /// - one text attribute of type LocalizedString "productName" + /// - one number attribute of type Number "price" + /// + /// will be returned in following json format: + /// + /// ``` + /// { + /// "productName": { + /// "en-US": "Terraforming Mars", + /// "ru-RU": "Покорение Марса" + /// }, + /// "price": 100 + /// } + /// ``` + /// + /// + /// + /// + /// + public static async Task> QueryInstancesJsonMultiLanguage( + this EAVService eavService, + Guid entityConfigurationId, + ProjectionQuery query, + CancellationToken cancellationToken = default + ) + { + var results = await eavService.QueryInstances( + entityConfigurationId, + query, + cancellationToken + ); + + var serializerOptions = new JsonSerializerOptions(eavService.JsonSerializerOptions); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + serializerOptions.Converters.Add(new LocalizedStringMultiLanguageSerializer()); + + return results.TransformResultDocuments( + r => JsonSerializer.SerializeToDocument(r, serializerOptions) + ); + } + + /// + /// Returns records in json serialized format. + /// LocalizedStrings are converted to a single language string of the language passed in parameters. + /// + /// EntityInstance with: + /// + /// - one text attribute of type LocalizedString "productName" + /// - one number attribute of type Number "price" + /// + /// will be returned in following json format: + /// + /// ``` + /// { + /// "productName": "Terraforming Mars", + /// "price": 100 + /// } + /// ``` + /// + /// + /// + /// Language to use from all localized strings. Only this language strings will be returned. + /// If main language will not be found, this language will be tried. Defaults to en-US. + /// + /// + public static async Task> QueryInstancesJsonSingleLanguage( + this EAVService eavService, + Guid entityConfigurationId, + ProjectionQuery query, + string language = "en-US", + string fallbackLanguage = "en-US", + CancellationToken cancellationToken = default + ) + { + var results = await eavService.QueryInstances( + entityConfigurationId, + query, + cancellationToken + ); + + var serializerOptions = new JsonSerializerOptions(eavService.JsonSerializerOptions); + serializerOptions.Converters.Add(new EntityInstanceViewModelToJsonSerializer()); + serializerOptions.Converters.Add(new LocalizedStringSingleLanguageSerializer(language, fallbackLanguage)); + + return results.TransformResultDocuments( + r => JsonSerializer.SerializeToDocument(r, serializerOptions) + ); + } +} diff --git a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs index 3ac9ed1..10745fb 100644 --- a/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs +++ b/CloudFabric.EAV.Service/MappingProfiles/EntityInstanceMappingProfile.cs @@ -14,25 +14,17 @@ public EntityInstanceProfile() CreateMap(); CreateMap(); + CreateMap(); CreateMap(); + CreateMap(); CreateMap(); - CreateMap(); - - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap().ForMember(o => o.Children, - opt => opt.MapFrom(_ => new List()) - ); - - CreateMap(); - CreateMap(); - - CreateMap(); + CreateMap(); CreateMap(); CreateMap(); + + CreateMap(); } } diff --git a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs index 1876bea..5517111 100644 --- a/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs +++ b/CloudFabric.EAV.Service/Serialization/EntityInstanceFromDictionaryDeserializer.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; + using AutoMapper; using CloudFabric.EAV.Enums; @@ -8,16 +10,39 @@ namespace CloudFabric.EAV.Service.Serialization; -public abstract class InstanceFromDictionaryDeserializer where T: EntityInstanceViewModel + +/// +/// 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 the 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 InstanceFromDictionaryDeserializer { - internal IMapper _mapper { get; set; } + private readonly IMapper _mapper; - public abstract T Deserialize( - List attributesConfigurations, - Dictionary record - ); + public InstanceFromDictionaryDeserializer(IMapper mapper) + { + _mapper = mapper; + } - internal List ParseCategoryPaths(object? paths) + internal ReadOnlyCollection ParseCategoryPaths(object? paths) { var categoryPaths = new List(); if (paths is List pathsList) @@ -60,7 +85,7 @@ internal List ParseCategoryPaths(object? paths) categoryPaths = _mapper.Map>(pathsListModel); } - return categoryPaths; + return categoryPaths.AsReadOnly(); } internal AttributeInstanceViewModel DeserializeAttribute( @@ -173,39 +198,8 @@ internal 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( + public EntityInstanceViewModel Deserialize( List attributesConfigurations, Dictionary record ) @@ -223,48 +217,12 @@ public override EntityInstanceViewModel Deserialize( .Select(attributeConfig => DeserializeAttribute(attributeConfig, record[attributeConfig.MachineName]) ) - .ToList(), + .ToList().AsReadOnly(), + MachineName = record.ContainsKey("MachineName") ? (string)record["MachineName"]! : null, CategoryPaths = record.ContainsKey("CategoryPaths") ? ParseCategoryPaths(record["CategoryPaths"]) - : new List() + : new List().AsReadOnly() }; 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(), - MachineName = (string)record["MachineName"]! - }; - return category; - } } diff --git a/CloudFabric.EAV.Service/ValueAttributeService.cs b/CloudFabric.EAV.Service/ValueAttributeService.cs index 94a818f..314f58e 100644 --- a/CloudFabric.EAV.Service/ValueAttributeService.cs +++ b/CloudFabric.EAV.Service/ValueAttributeService.cs @@ -44,7 +44,7 @@ internal async Task InitializeEntityConfigurationGeneratedValues(Guid entityConf } /// - /// Initalize generating value for attribute configuration within entity configuration based on attribute type. + /// Initialize generating value for attribute configuration within entity configuration based on attribute type. /// Already initialized values will not be overriten or changed. /// /// diff --git a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs index 40ae901..ef07bc5 100644 --- a/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs +++ b/CloudFabric.EAV.Tests/BaseQueryTests/BaseQueryTests.cs @@ -21,14 +21,14 @@ namespace CloudFabric.EAV.Tests.BaseQueryTests; public abstract class BaseQueryTests { - protected EAVEntityInstanceService _eavEntityInstanceService; + protected EAVService _eavService; protected EAVCategoryService _eavCategoryService; protected ValueAttributeService _valueAttributeService; protected IEventStore _eventStore; protected IStore _store; - protected ILogger _eiLogger; - protected ILogger _cLogger; + protected ILogger _eavServiceLogger; + protected ILogger _eavCategoryServiceLogger; protected virtual TimeSpan ProjectionsUpdateDelay { get; set; } = TimeSpan.FromMilliseconds(0); @@ -43,12 +43,12 @@ public abstract class BaseQueryTests public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _eiLogger = loggerFactory.CreateLogger(); - _cLogger = loggerFactory.CreateLogger(); + _eavServiceLogger = loggerFactory.CreateLogger(); + _eavCategoryServiceLogger = loggerFactory.CreateLogger(); var eiConfiguration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); } ); @@ -143,8 +143,8 @@ public async Task SetUp() eiMapper ); - _eavEntityInstanceService = new EAVEntityInstanceService( - _eiLogger, + _eavService = new EAVService( + _eavServiceLogger, eiMapper, new JsonSerializerOptions { @@ -158,17 +158,11 @@ public async Task SetUp() ); _eavCategoryService = new EAVCategoryService( - _cLogger, + _eavCategoryServiceLogger, + _eavService, cMapper, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase - }, aggregateRepositoryFactory, - projectionRepositoryFactory, - new EventUserInfo(Guid.NewGuid()), - _valueAttributeService + new EventUserInfo(Guid.NewGuid()) ); } } diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs index e056dff..d061ea7 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTests.cs @@ -22,17 +22,17 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests private const string _asusGamingLaptopsCategoryMachineName = "asus-gaming-laptops"; private const string _rogAsusGamingLaptopsCategoryMachineName = "rog-gaming-laptops"; - private async Task<(HierarchyViewModel tree, - CategoryViewModel laptopsCategory, - CategoryViewModel gamingLaptopsCategory, - CategoryViewModel officeLaptopsCategory, - CategoryViewModel asusGamingLaptopsCategory, - CategoryViewModel rogAsusGamingLaptopsCategory)> BuildTestTreeAsync() + private async Task<(CategoryTreeViewModel tree, + EntityInstanceViewModel laptopsCategory, + EntityInstanceViewModel gamingLaptopsCategory, + EntityInstanceViewModel officeLaptopsCategory, + EntityInstanceViewModel asusGamingLaptopsCategory, + EntityInstanceViewModel rogAsusGamingLaptopsCategory)> BuildTestTreeAsync() { // Create config for categories EntityConfigurationCreateRequest categoryConfigurationCreateRequest = EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(0, 9); - (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? categoryConfiguration, _) = await _eavService.CreateEntityConfiguration( categoryConfigurationCreateRequest, CancellationToken.None ); @@ -46,12 +46,12 @@ public abstract class CategoryTests : BaseQueryTests.BaseQueryTests EntityConfigurationId = categoryConfiguration!.Id }; - (HierarchyViewModel createdTree, _) = await _eavCategoryService.CreateCategoryTreeAsync(treeRequest, + (CategoryTreeViewModel createdTree, _) = await _eavCategoryService.CreateCategoryTreeAsync(treeRequest, categoryConfigurationCreateRequest.TenantId, CancellationToken.None ); - (CategoryViewModel laptopsCategory, _) = + (EntityInstanceViewModel laptopsCategory, _) = await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, null, @@ -61,7 +61,7 @@ await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCat 9 )); - (CategoryViewModel gamingLaptopsCategory, _) = + (EntityInstanceViewModel gamingLaptopsCategory, _) = await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, laptopsCategory.Id, @@ -71,7 +71,7 @@ await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCat 9 )); - (CategoryViewModel officeLaptopsCategory, _) = + (EntityInstanceViewModel officeLaptopsCategory, _) = await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, laptopsCategory.Id, @@ -81,7 +81,7 @@ await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCat 9 )); - (CategoryViewModel asusGamingLaptopsCategory, _) = + (EntityInstanceViewModel asusGamingLaptopsCategory, _) = await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, gamingLaptopsCategory.Id, @@ -91,7 +91,7 @@ await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCat 9 )); - (CategoryViewModel rogAsusGamingLaptopsCategory, _) = + (EntityInstanceViewModel rogAsusGamingLaptopsCategory, _) = await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCategoryInstanceRequest(categoryConfiguration.Id, createdTree.Id, asusGamingLaptopsCategory.Id, @@ -109,7 +109,7 @@ await _eavCategoryService.CreateCategoryInstance(EntityInstanceFactory.CreateCat [TestMethod] public async Task CreateCategory_Success() { - (_, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, _, _, _) = + (_, EntityInstanceViewModel laptopsCategory, EntityInstanceViewModel gamingLaptopsCategory, _, _, _) = await BuildTestTreeAsync(); laptopsCategory.Id.Should().NotBeEmpty(); @@ -121,9 +121,9 @@ public async Task CreateCategory_Success() [TestMethod] public async Task GetTreeViewAsync() { - (HierarchyViewModel createdTree, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, - CategoryViewModel officeLaptopsCategory, CategoryViewModel asusGamingLaptopsCategory, - CategoryViewModel rogAsusGamingLaptopsCategory) = await BuildTestTreeAsync(); + (CategoryTreeViewModel createdTree, EntityInstanceViewModel laptopsCategory, EntityInstanceViewModel gamingLaptopsCategory, + EntityInstanceViewModel officeLaptopsCategory, EntityInstanceViewModel asusGamingLaptopsCategory, + EntityInstanceViewModel rogAsusGamingLaptopsCategory) = await BuildTestTreeAsync(); List list = await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id); @@ -163,9 +163,9 @@ public async Task GetTreeViewAsync() [TestMethod] public async Task GetTreeViewAsync_CategoryNofFound() { - (HierarchyViewModel createdTree, CategoryViewModel _, CategoryViewModel _, - CategoryViewModel _, CategoryViewModel _, - CategoryViewModel _) = await BuildTestTreeAsync(); + (CategoryTreeViewModel createdTree, EntityInstanceViewModel _, EntityInstanceViewModel _, + EntityInstanceViewModel _, EntityInstanceViewModel _, + EntityInstanceViewModel _) = await BuildTestTreeAsync(); Func action = async () => await _eavCategoryService.GetCategoryTreeViewAsync(createdTree.Id, Guid.NewGuid()); @@ -175,12 +175,12 @@ public async Task GetTreeViewAsync_CategoryNofFound() [TestMethod] public async Task GetSubcategoriesBranch_Success() { - (HierarchyViewModel createdTree, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, + (CategoryTreeViewModel createdTree, EntityInstanceViewModel laptopsCategory, EntityInstanceViewModel gamingLaptopsCategory, _, _, _) = await BuildTestTreeAsync(); await Task.Delay(ProjectionsUpdateDelay); var categoryPathValue = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}"; - ProjectionQueryResult subcategories12 = await _eavEntityInstanceService.QueryInstances( + ProjectionQueryResult subcategories12 = await _eavService.QueryInstances( createdTree.EntityConfigurationId, new ProjectionQuery { @@ -198,9 +198,9 @@ public async Task GetSubcategoriesBranch_Success() [TestMethod] public async Task GetSubcategories_Success() { - (HierarchyViewModel createdTree, CategoryViewModel laptopsCategory, - CategoryViewModel gamingLaptopsCategory, CategoryViewModel officeLaptopsCategory, - CategoryViewModel asusGamingLaptops, CategoryViewModel _) = await BuildTestTreeAsync(); + (CategoryTreeViewModel createdTree, EntityInstanceViewModel laptopsCategory, + EntityInstanceViewModel gamingLaptopsCategory, EntityInstanceViewModel officeLaptopsCategory, + EntityInstanceViewModel asusGamingLaptops, EntityInstanceViewModel _) = await BuildTestTreeAsync(); var subcategories = await _eavCategoryService.GetSubcategories(createdTree.Id, null); subcategories.Count.Should().Be(1); @@ -221,9 +221,9 @@ public async Task GetSubcategories_Success() [TestMethod] public async Task GetSubcategories_TreeNotFound() { - (HierarchyViewModel createdTree, CategoryViewModel _, - CategoryViewModel _, CategoryViewModel _, - CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); + (CategoryTreeViewModel createdTree, EntityInstanceViewModel _, + EntityInstanceViewModel _, EntityInstanceViewModel _, + EntityInstanceViewModel _, EntityInstanceViewModel _) = await BuildTestTreeAsync(); Func action = async () => await _eavCategoryService.GetSubcategories(Guid.NewGuid()); @@ -233,9 +233,9 @@ public async Task GetSubcategories_TreeNotFound() [TestMethod] public async Task GetSubcategories_ParentNotFound() { - (HierarchyViewModel createdTree, CategoryViewModel _, - CategoryViewModel _, CategoryViewModel _, - CategoryViewModel _, CategoryViewModel _) = await BuildTestTreeAsync(); + (CategoryTreeViewModel createdTree, EntityInstanceViewModel _, + EntityInstanceViewModel _, EntityInstanceViewModel _, + EntityInstanceViewModel _, EntityInstanceViewModel _) = await BuildTestTreeAsync(); var result = await _eavCategoryService.GetSubcategories(createdTree.Id, parentId: Guid.NewGuid()); result.Should().BeEmpty(); @@ -244,13 +244,13 @@ public async Task GetSubcategories_ParentNotFound() [TestMethod] public async Task MoveAndGetItemsFromCategory_Success() { - (HierarchyViewModel createdTree, CategoryViewModel laptopsCategory, CategoryViewModel gamingLaptopsCategory, - _, CategoryViewModel asusGamingLaptops, CategoryViewModel rogAsusGamingLaptops) = + (CategoryTreeViewModel createdTree, EntityInstanceViewModel laptopsCategory, EntityInstanceViewModel gamingLaptopsCategory, + _, EntityInstanceViewModel asusGamingLaptops, EntityInstanceViewModel rogAsusGamingLaptops) = await BuildTestTreeAsync(); EntityConfigurationCreateRequest itemEntityConfig = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? itemEntityConfiguration, _) = await _eavService.CreateEntityConfiguration( itemEntityConfig, CancellationToken.None ); @@ -260,11 +260,11 @@ public async Task MoveAndGetItemsFromCategory_Success() EntityInstanceCreateRequest itemInstanceRequest = EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(itemEntityConfiguration.Id); - var (_, _) = await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); + var (_, _) = await _eavService.CreateEntityInstance(itemInstanceRequest); (EntityInstanceViewModel createdItemInstance2, _) = - await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); - (createdItemInstance2, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance2.Id, + await _eavService.CreateEntityInstance(itemInstanceRequest); + (createdItemInstance2, _) = await _eavCategoryService.UpdateCategoryPath(createdItemInstance2.Id, createdItemInstance2.PartitionKey, createdTree.Id, asusGamingLaptops.Id, @@ -272,8 +272,8 @@ public async Task MoveAndGetItemsFromCategory_Success() ); (EntityInstanceViewModel createdItemInstance3, _) = - await _eavEntityInstanceService.CreateEntityInstance(itemInstanceRequest); - (_, _) = await _eavEntityInstanceService.UpdateCategoryPath(createdItemInstance3.Id, + await _eavService.CreateEntityInstance(itemInstanceRequest); + (_, _) = await _eavCategoryService.UpdateCategoryPath(createdItemInstance3.Id, createdItemInstance2.PartitionKey, createdTree.Id, rogAsusGamingLaptops.Id, @@ -285,7 +285,7 @@ public async Task MoveAndGetItemsFromCategory_Success() var pathFilterValue121 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom121 = await _eavEntityInstanceService.QueryInstances( + ProjectionQueryResult itemsFrom121 = await _eavService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { @@ -299,7 +299,7 @@ public async Task MoveAndGetItemsFromCategory_Success() var pathFilterValue1211 = $"/{_laptopsCategoryMachineName}/{_gamingLaptopsCategoryMachineName}/{_asusGamingLaptopsCategoryMachineName}/{_rogAsusGamingLaptopsCategoryMachineName}"; - ProjectionQueryResult itemsFrom1211 = await _eavEntityInstanceService.QueryInstances( + ProjectionQueryResult itemsFrom1211 = await _eavService.QueryInstances( itemEntityConfiguration.Id, new ProjectionQuery { diff --git a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs index 00175c7..4acf081 100644 --- a/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs +++ b/CloudFabric.EAV.Tests/CategoryTests/CategoryTestsPostgresql.cs @@ -27,7 +27,9 @@ public CategoryTestsPostgresql() "eav_tests_event_store", "eav_tests_item_store" ); - _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory(new LoggerFactory(), connectionString); + _projectionRepositoryFactory = new PostgresqlProjectionRepositoryFactory( + new LoggerFactory(), connectionString, includeDebugInformation: true + ); _store = new PostgresqlStore(connectionString, "eav_test_item_store"); diff --git a/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj b/CloudFabric.EAV.Tests/CloudFabric.EAV.Tests.csproj index 8ff2bc3..af18822 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/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs index 6772831..22e10bd 100644 --- a/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs +++ b/CloudFabric.EAV.Tests/EntityInstanceQueryingTests/EntityInstanceQueryingTests.cs @@ -16,6 +16,8 @@ using System.Text.Json; +using CloudFabric.EAV.Service; + // ReSharper disable AsyncConverter.ConfigureAwaitHighlighting namespace CloudFabric.EAV.Tests.EntityInstanceQueryingTests; @@ -28,14 +30,14 @@ public async Task TestCreateInstanceAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); - EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( createdConfiguration.Id ); @@ -45,7 +47,7 @@ public async Task TestCreateInstanceAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); + await _eavService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -59,7 +61,7 @@ public async Task TestCreateInstanceAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavEntityInstanceService + ProjectionQueryResult? results = await _eavService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -73,12 +75,12 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( createdConfiguration.Id ); @@ -88,7 +90,7 @@ public async Task TestCreateInstanceUpdateAndQuery() EntityInstanceFactory.CreateValidBoardGameEntityInstanceCreateRequest(createdConfiguration.Id); (EntityInstanceViewModel createdInstance, ProblemDetails createProblemDetails) = - await _eavEntityInstanceService.CreateEntityInstance(instanceCreateRequest); + await _eavService.CreateEntityInstance(instanceCreateRequest); createdInstance.EntityConfigurationId.Should().Be(instanceCreateRequest.EntityConfigurationId); createdInstance.TenantId.Should().Be(instanceCreateRequest.TenantId); @@ -103,7 +105,7 @@ public async Task TestCreateInstanceUpdateAndQuery() await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavEntityInstanceService + ProjectionQueryResult? results = await _eavService .QueryInstances(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -120,7 +122,7 @@ public async Task TestCreateInstanceUpdateAndQuery() }; (EntityInstanceViewModel updateResult, ProblemDetails updateErrors) = - await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToString(), + await _eavService.UpdateEntityInstance(createdConfiguration.Id.ToString(), new EntityInstanceUpdateRequest { Id = createdInstance.Id, @@ -133,7 +135,7 @@ await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToS await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? searchResultsAfterUpdate = await _eavEntityInstanceService + ProjectionQueryResult? searchResultsAfterUpdate = await _eavService .QueryInstances(createdConfiguration.Id, query); searchResultsAfterUpdate?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -150,7 +152,7 @@ await _eavEntityInstanceService.UpdateEntityInstance(createdConfiguration.Id.ToS .String.Should() .Be("Азул 2"); - var resultsJson = await _eavEntityInstanceService + var resultsJson = await _eavService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); var resultString = JsonSerializer.Serialize(resultsJson); diff --git a/CloudFabric.EAV.Tests/JsonSerializationTests.cs b/CloudFabric.EAV.Tests/JsonSerializationTests.cs index 22c5a58..e6a46b2 100644 --- a/CloudFabric.EAV.Tests/JsonSerializationTests.cs +++ b/CloudFabric.EAV.Tests/JsonSerializationTests.cs @@ -3,6 +3,7 @@ using CloudFabric.EAV.Domain.Models; using CloudFabric.EAV.Models.RequestModels; using CloudFabric.EAV.Models.ViewModels; +using CloudFabric.EAV.Service; using CloudFabric.EAV.Tests.Factories; using CloudFabric.EventSourcing.EventStore; using CloudFabric.EventSourcing.EventStore.InMemory; @@ -63,14 +64,14 @@ public async Task TestCreateInstanceMultiLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); - EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -80,7 +81,7 @@ public async Task TestCreateInstanceMultiLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonMultiLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavEntityInstanceService.CreateEntityInstance( + await _eavService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -102,7 +103,7 @@ await _eavEntityInstanceService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavEntityInstanceService + ProjectionQueryResult? results = await _eavService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -139,7 +140,7 @@ await _eavEntityInstanceService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -147,7 +148,7 @@ await _eavEntityInstanceService.CreateEntityInstance( resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") .GetProperty("ru-RU").GetString().Should().Be("Азул"); - ProjectionQueryResult resultsJsonSingleLanguage = await _eavEntityInstanceService + ProjectionQueryResult resultsJsonSingleLanguage = await _eavService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "en-US" ); @@ -155,7 +156,7 @@ await _eavEntityInstanceService.CreateEntityInstance( resultsJsonSingleLanguage.Records.First().Document.RootElement.GetProperty("name") .GetString().Should().Be("Azul"); - ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavEntityInstanceService + ProjectionQueryResult? resultsJsonSingleLanguageRu = await _eavService .QueryInstancesJsonSingleLanguage( createdConfiguration.Id, query, "ru-RU" ); @@ -169,14 +170,14 @@ await _eavEntityInstanceService.CreateEntityInstance( var firstDocumentId = resultsJsonMultiLanguage.Records[0]?.Document!.RootElement.GetProperty("id").GetString(); - var oneInstanceJsonMultiLang = await _eavEntityInstanceService.GetEntityInstanceJsonMultiLanguage( + var oneInstanceJsonMultiLang = await _eavService.GetEntityInstanceJsonMultiLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()! ); oneInstanceJsonMultiLang.RootElement.GetProperty("name").GetProperty("en-US").GetString().Should().Be("Azul"); - var oneInstanceJsonSingleLang = await _eavEntityInstanceService.GetEntityInstanceJsonSingleLanguage( + var oneInstanceJsonSingleLang = await _eavService.GetEntityInstanceJsonSingleLanguage( Guid.Parse(firstDocumentId!), createdInstance.RootElement.GetProperty("entityConfigurationId").GetString()!, "en-US" @@ -191,12 +192,12 @@ public async Task TestCreateInstanceSingleLangAndQuery() EntityConfigurationCreateRequest configurationCreateRequest = EntityConfigurationFactory.CreateBoardGameEntityConfigurationCreateRequest(); - (EntityConfigurationViewModel? createdConfiguration, _) = await _eavEntityInstanceService.CreateEntityConfiguration( + (EntityConfigurationViewModel? createdConfiguration, _) = await _eavService.CreateEntityConfiguration( configurationCreateRequest, CancellationToken.None ); - EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( createdConfiguration!.Id ); @@ -206,7 +207,7 @@ public async Task TestCreateInstanceSingleLangAndQuery() .CreateValidBoardGameEntityInstanceCreateRequestJsonSingleLanguage(createdConfiguration.Id); (JsonDocument createdInstance, ProblemDetails createProblemDetails) = - await _eavEntityInstanceService.CreateEntityInstance( + await _eavService.CreateEntityInstance( instanceCreateRequest, configuration.Id, configuration.TenantId.Value @@ -228,7 +229,7 @@ await _eavEntityInstanceService.CreateEntityInstance( await Task.Delay(ProjectionsUpdateDelay); - ProjectionQueryResult? results = await _eavEntityInstanceService + ProjectionQueryResult? results = await _eavService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); results?.TotalRecordsFound.Should().BeGreaterThan(0); @@ -265,7 +266,7 @@ await _eavEntityInstanceService.CreateEntityInstance( .Should() .Be(createdInstance.RootElement.GetProperty("release_date").GetProperty("from").GetDateTime()); - ProjectionQueryResult resultsJsonMultiLanguage = await _eavEntityInstanceService + ProjectionQueryResult resultsJsonMultiLanguage = await _eavService .QueryInstancesJsonMultiLanguage(createdConfiguration.Id, query); resultsJsonMultiLanguage.Records.First().Document.RootElement.GetProperty("name") @@ -275,16 +276,16 @@ await _eavEntityInstanceService.CreateEntityInstance( [TestMethod] public async Task CreateCategoryInstance() { - JsonSerializerOptions _serializerOptions = new JsonSerializerOptions(); - _serializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + JsonSerializerOptions serializerOptions = new JsonSerializerOptions(); + serializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; EntityConfigurationCreateRequest categoryConfigurationRequest = EntityConfigurationFactory.CreateBoardGameCategoryConfigurationCreateRequest(); (EntityConfigurationViewModel? createdCategoryConfiguration, _) = - await _eavEntityInstanceService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); + await _eavService.CreateEntityConfiguration(categoryConfigurationRequest, CancellationToken.None); - (HierarchyViewModel hierarchy, _) = await _eavCategoryService.CreateCategoryTreeAsync( + (CategoryTreeViewModel hierarchy, _) = await _eavCategoryService.CreateCategoryTreeAsync( new CategoryTreeCreateRequest { EntityConfigurationId = createdCategoryConfiguration!.Id, @@ -295,7 +296,7 @@ public async Task CreateCategoryInstance() await ProjectionsRebuildProcessor.RebuildProjectionsThatRequireRebuild(); - EntityConfigurationViewModel configuration = await _eavEntityInstanceService.GetEntityConfiguration( + EntityConfigurationViewModel configuration = await _eavService.GetEntityConfiguration( createdCategoryConfiguration!.Id ); @@ -316,14 +317,14 @@ public async Task CreateCategoryInstance() } }; - var results = await _eavEntityInstanceService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); + var results = await _eavService.QueryInstancesJsonSingleLanguage(createdCategoryConfiguration.Id, query); var resultDocument = results?.Records.Select(r => r.Document).First(); resultDocument!.RootElement.GetProperty("entityConfigurationId").GetString().Should().Be(createdCategoryConfiguration.Id.ToString()); resultDocument!.RootElement.GetProperty("tenantId").GetString().Should().Be(createdCategoryConfiguration.TenantId.ToString()); - JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), _serializerOptions)! + JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), serializerOptions)! .First().TreeId.Should().Be(hierarchy.Id); (createdCategory, _) = await _eavCategoryService.CreateCategoryInstance( @@ -338,7 +339,7 @@ public async Task CreateCategoryInstance() createdCategory!.RootElement.GetProperty("entityConfigurationId").GetString().Should().Be(createdCategoryConfiguration.Id.ToString()); createdCategory!.RootElement.GetProperty("tenantId").GetString().Should().Be(createdCategoryConfiguration.TenantId.ToString()); - JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), _serializerOptions)! + JsonSerializer.Deserialize>(resultDocument!.RootElement.GetProperty("categoryPaths"), serializerOptions)! .First().TreeId.Should().Be(hierarchy.Id); } } diff --git a/CloudFabric.EAV.Tests/Tests.cs b/CloudFabric.EAV.Tests/Tests.cs index b684eeb..e5b3ffa 100644 --- a/CloudFabric.EAV.Tests/Tests.cs +++ b/CloudFabric.EAV.Tests/Tests.cs @@ -42,13 +42,13 @@ public class Tests { private AggregateRepositoryFactory _aggregateRepositoryFactory; - private EAVEntityInstanceService _eavEntityInstanceService; + private EAVService _eavEntityInstanceService; private SerialCounterService _entitySerialCounterService; private IEventStore _eventStore; private IStore _store; - private ILogger _eiLogger; + private ILogger _eavServiceLogger; private PostgresqlProjectionRepositoryFactory _projectionRepositoryFactory; private IMapper _mapper; @@ -56,11 +56,11 @@ public class Tests public async Task SetUp() { var loggerFactory = new LoggerFactory(); - _eiLogger = loggerFactory.CreateLogger(); + _eavServiceLogger = loggerFactory.CreateLogger(); var configuration = new MapperConfiguration(cfg => { - cfg.AddMaps(Assembly.GetAssembly(typeof(EAVEntityInstanceService))); + cfg.AddMaps(Assembly.GetAssembly(typeof(EAVService))); } ); _mapper = configuration.CreateMapper(); @@ -136,8 +136,8 @@ public async Task SetUp() _entitySerialCounterService = new SerialCounterService(new StoreRepository(_store)); - _eavEntityInstanceService = new EAVEntityInstanceService( - _eiLogger, + _eavEntityInstanceService = new EAVService( + _eavServiceLogger, _mapper, new JsonSerializerOptions() { diff --git a/CloudFabric.EAV.sln.DotSettings b/CloudFabric.EAV.sln.DotSettings index 4bfcbe9..aed0e44 100644 --- a/CloudFabric.EAV.sln.DotSettings +++ b/CloudFabric.EAV.sln.DotSettings @@ -1,4 +1,5 @@  + EAV True True True