diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7016c13..d000348 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,7 +7,7 @@ on:
branches: [ "main" ]
env:
- PACKAGE_VERSION: 1.2.13
+ PACKAGE_VERSION: 1.3.0
jobs:
diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj
index 378aa65..2d59764 100644
--- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj
+++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.LocalEventSourcingPackages.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj
index 697d9f5..58c7d33 100644
--- a/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj
+++ b/CloudFabric.EAV.Domain/CloudFabric.EAV.Domain.csproj
@@ -12,8 +12,8 @@
-
-
+
+
diff --git a/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs
new file mode 100644
index 0000000..9ab9a70
--- /dev/null
+++ b/CloudFabric.EAV.Domain/Events/Instance/CategoryCreated.cs
@@ -0,0 +1,30 @@
+using CloudFabric.EventSourcing.EventStore;
+
+namespace CloudFabric.EAV.Domain.Models;
+
+public record CategoryCreated : Event
+{
+ public CategoryCreated()
+ {
+
+ }
+
+ public CategoryCreated(Guid id,
+ string machineName,
+ Guid entityConfigurationId,
+ List attributes,
+ Guid? tenantId)
+ {
+ TenantId = tenantId;
+ Attributes = attributes;
+ EntityConfigurationId = entityConfigurationId;
+ AggregateId = id;
+ MachineName = machineName;
+ }
+
+ public Guid EntityConfigurationId { get; set; }
+ public List Attributes { get; set; }
+ public Guid? TenantId { get; set; }
+ public string MachineName { get; set; }
+
+}
diff --git a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs
index 990ca06..11cfe3d 100644
--- a/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs
+++ b/CloudFabric.EAV.Domain/Events/Instance/Entity/EntityCategoryPathChanged.cs
@@ -10,16 +10,24 @@ public EntityCategoryPathChanged()
{
}
- public EntityCategoryPathChanged(Guid id, Guid entityConfigurationId, Guid categoryTreeId, string categoryPath)
+ public EntityCategoryPathChanged(Guid id,
+ Guid entityConfigurationId,
+ Guid categoryTreeId,
+ string categoryPath,
+ Guid? parentId)
{
AggregateId = id;
EntityConfigurationId = entityConfigurationId;
CategoryPath = categoryPath;
CategoryTreeId = categoryTreeId;
+ ParentId = parentId;
+ ParentMachineName = string.IsNullOrEmpty(categoryPath) ? "" : categoryPath.Split('/').Last(x => !string.IsNullOrEmpty(x));
}
public string CategoryPath { get; set; }
public Guid EntityConfigurationId { get; set; }
public Guid CategoryTreeId { get; set; }
+ public Guid? ParentId { get; set; }
+ public string ParentMachineName { get; set; }
}
diff --git a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs
index 1c1afc8..ba5eb31 100644
--- a/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs
+++ b/CloudFabric.EAV.Domain/Models/Attributes/ValueFromListOptionConfiguration.cs
@@ -1,5 +1,7 @@
using System.Text.RegularExpressions;
+using CloudFabric.EAV.Domain.Utilities.Extensions;
+
namespace CloudFabric.EAV.Domain.Models.Attributes;
public class ValueFromListOptionConfiguration
@@ -11,9 +13,7 @@ public ValueFromListOptionConfiguration(string name, string? machineName)
if (string.IsNullOrEmpty(machineName))
{
- machineName = name.Replace(" ", "_");
- var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100));
- machineName = specSymbolsRegex.Replace(machineName, "").ToLower();
+ machineName = name.SanitizeForMachineName();
}
MachineName = machineName;
diff --git a/CloudFabric.EAV.Domain/Models/Category.cs b/CloudFabric.EAV.Domain/Models/Category.cs
index 8d9f539..b7fb109 100644
--- a/CloudFabric.EAV.Domain/Models/Category.cs
+++ b/CloudFabric.EAV.Domain/Models/Category.cs
@@ -5,24 +5,42 @@ namespace CloudFabric.EAV.Domain.Models;
public class Category : EntityInstanceBase
{
+ public string MachineName { get; set; }
public Category(IEnumerable events) : base(events)
{
}
- public Category(Guid id, Guid entityConfigurationId, List attributes, Guid? tenantId)
- : base(id, entityConfigurationId, attributes, tenantId)
+ 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
- ) : base(id, entityConfigurationId, attributes, tenantId)
+ ) : this(id, machineName, entityConfigurationId, attributes, tenantId)
+ {
+ Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath, parentId));
+ }
+
+ public void On(CategoryCreated @event)
{
- Apply(new EntityCategoryPathChanged(id, EntityConfigurationId, categoryTreeId, categoryPath));
+ 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 608929e..1b2ac03 100644
--- a/CloudFabric.EAV.Domain/Models/CategoryPath.cs
+++ b/CloudFabric.EAV.Domain/Models/CategoryPath.cs
@@ -4,4 +4,6 @@ public class CategoryPath
{
public Guid TreeId { get; set; }
public string Path { get; set; }
+ public Guid? ParentId { get; set; }
+ public string ParentMachineName { get; set; }
}
diff --git a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs
index 99f78a7..3ffa85a 100644
--- a/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs
+++ b/CloudFabric.EAV.Domain/Models/EntityInstanceBase.cs
@@ -9,7 +9,7 @@ namespace CloudFabric.EAV.Domain.Models;
public class EntityInstanceBase : AggregateBase
{
- public EntityInstanceBase(IEnumerable events) : base(events)
+ public EntityInstanceBase(IEnumerable events) : base(events)
{
}
@@ -19,6 +19,10 @@ public EntityInstanceBase(Guid id, Guid entityConfigurationId, List EntityConfigurationId.ToString();
public List CategoryPaths { get; protected set; }
@@ -54,9 +58,9 @@ public void On(EntityInstanceCreated @event)
CategoryPaths = new List();
}
- public void ChangeCategoryPath(Guid treeId, string categoryPath)
+ public void ChangeCategoryPath(Guid treeId, string categoryPath, Guid parentId)
{
- Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath));
+ Apply(new EntityCategoryPathChanged(Id, EntityConfigurationId, treeId, categoryPath, parentId));
}
public void On(EntityCategoryPathChanged @event)
@@ -64,11 +68,17 @@ public void On(EntityCategoryPathChanged @event)
CategoryPath? categoryPath = CategoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId);
if (categoryPath == null)
{
- CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId, Path = @event.CategoryPath });
+ CategoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId,
+ Path = @event.CategoryPath,
+ ParentId = @event.ParentId,
+ ParentMachineName = @event.ParentMachineName
+ });
}
else
{
categoryPath.Path = @event.CategoryPath;
+ categoryPath.ParentMachineName = @event.ParentMachineName;
+ categoryPath.ParentId = @event.ParentId;
}
}
diff --git a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs
index b2827e4..8bc5d8e 100644
--- a/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs
+++ b/CloudFabric.EAV.Domain/Projections/AttributeConfigurationProjection/AttributeConfigurationProjectionBuilder.cs
@@ -26,8 +26,9 @@ public class AttributeConfigurationProjectionBuilder : ProjectionBuilder
{
public AttributeConfigurationProjectionBuilder(
- ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _
- ) : base(projectionRepositoryFactory)
+ ProjectionRepositoryFactory projectionRepositoryFactory,
+ ProjectionOperationIndexSelector indexSelector
+ ) : base(projectionRepositoryFactory, indexSelector)
{
}
diff --git a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs
index 8a96c1d..1dded58 100644
--- a/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs
+++ b/CloudFabric.EAV.Domain/Projections/EntityConfigurationProjection/EntityConfigurationProjectionBuilder.cs
@@ -15,8 +15,9 @@ public class EntityConfigurationProjectionBuilder : ProjectionBuilder>
{
public EntityConfigurationProjectionBuilder(
- ProjectionRepositoryFactory projectionRepositoryFactory, AggregateRepositoryFactory _
- ) : base(projectionRepositoryFactory)
+ ProjectionRepositoryFactory projectionRepositoryFactory,
+ ProjectionOperationIndexSelector indexSelector
+ ) : base(projectionRepositoryFactory, indexSelector)
{
}
diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs
index 27da37e..fe2393f 100644
--- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs
+++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/EntityInstanceProjectionBuilder.cs
@@ -31,7 +31,8 @@ namespace CloudFabric.EAV.Domain.Projections.EntityInstanceProjection;
///
public class EntityInstanceProjectionBuilder : ProjectionBuilder,
IHandleEvent,
- // IHandleEvent,
+ IHandleEvent,
+// IHandleEvent,
IHandleEvent,
// IHandleEvent,
IHandleEvent,
@@ -41,8 +42,9 @@ public class EntityInstanceProjectionBuilder : ProjectionBuilder,
public EntityInstanceProjectionBuilder(
ProjectionRepositoryFactory projectionRepositoryFactory,
- AggregateRepositoryFactory aggregateRepositoryFactory
- ) : base(projectionRepositoryFactory)
+ AggregateRepositoryFactory aggregateRepositoryFactory,
+ ProjectionOperationIndexSelector indexSelector
+ ) : base(projectionRepositoryFactory, indexSelector)
{
_aggregateRepositoryFactory = aggregateRepositoryFactory;
}
@@ -134,14 +136,20 @@ await UpdateDocument(
List categoryPaths =
categoryPathsObj as List ?? new List();
CategoryPath? categoryPath = categoryPaths.FirstOrDefault(x => x.TreeId == @event.CategoryTreeId);
+
if (categoryPath == null)
{
- categoryPaths.Add(new CategoryPath { Path = @event.CategoryPath, TreeId = @event.CategoryTreeId }
- );
+ categoryPaths.Add(new CategoryPath { TreeId = @event.CategoryTreeId,
+ Path = @event.CategoryPath,
+ ParentId = @event.ParentId,
+ ParentMachineName = @event.ParentMachineName
+ });
}
else
{
categoryPath.Path = @event.CategoryPath;
+ categoryPath.ParentMachineName = @event.ParentMachineName;
+ categoryPath.ParentId = @event.ParentId;
}
document["CategoryPaths"] = categoryPaths;
@@ -177,6 +185,35 @@ await UpsertDocument(
);
}
+ public async Task On(CategoryCreated @event)
+ {
+ ProjectionDocumentSchema projectionDocumentSchema =
+ await BuildProjectionDocumentSchemaForEntityConfigurationIdAsync(
+ @event.EntityConfigurationId
+ ).ConfigureAwait(false);
+
+ var document = new Dictionary
+ {
+ { "Id", @event.AggregateId },
+ { "EntityConfigurationId", @event.EntityConfigurationId },
+ { "TenantId", @event.TenantId },
+ { "CategoryPaths", new List() },
+ { "MachineName", @event.MachineName},
+ };
+
+ foreach (AttributeInstance attribute in @event.Attributes)
+ {
+ document.Add(attribute.ConfigurationAttributeMachineName, attribute.GetValue());
+ }
+
+ await UpsertDocument(
+ projectionDocumentSchema,
+ document,
+ @event.PartitionKey,
+ @event.Timestamp
+ );
+ }
+
private async Task BuildProjectionDocumentSchemaForEntityConfigurationIdAsync(
Guid entityConfigurationId
)
diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs
index 3ce1e85..3c7338f 100644
--- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs
+++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionAttributesSchemaFactory.cs
@@ -240,6 +240,7 @@ public static ProjectionDocumentPropertySchema GetArrayAttributeSchema(
EavAttributeType.HtmlText => null,
EavAttributeType.EntityReference => null,
EavAttributeType.ValueFromList => null,
+ EavAttributeType.Money => null,
EavAttributeType.LocalizedText => GetLocalizedTextAttributeNestedProperties(),
EavAttributeType.DateRange => GetDateAttributeNestedProperties(),
EavAttributeType.Image => GetImageAttributeNestedProperties(),
@@ -441,6 +442,22 @@ private static List GetCategoryPathsNestedProp
IsRetrievable = true,
IsFilterable = true,
IsSortable = true
+ },
+ new ()
+ {
+ PropertyName = "ParentMachineName",
+ PropertyType = TypeCode.String,
+ IsRetrievable = true,
+ IsFilterable = true,
+ IsSortable = true
+ },
+ new ()
+ {
+ PropertyName = "ParentId",
+ PropertyType = TypeCode.Object,
+ IsRetrievable = true,
+ IsFilterable = true,
+ IsSortable = true
}
};
}
diff --git a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs
index f5e978e..e94771d 100644
--- a/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs
+++ b/CloudFabric.EAV.Domain/Projections/EntityInstanceProjection/ProjectionDocumentSchemaFactory.cs
@@ -44,10 +44,25 @@ List attributeConfigurations
}
);
+ schema.Properties.Add(
+ new ProjectionDocumentPropertySchema
+ {
+ PropertyName = "MachineName",
+ PropertyType = TypeCode.String,
+ IsKey = false,
+ IsSearchable = true,
+ IsRetrievable = true,
+ IsFilterable = true,
+ IsSortable = false,
+ IsFacetable = false
+ }
+ );
+
schema.Properties.Add(
ProjectionAttributesSchemaFactory.GetCategoryPathsAttributeSchema()
);
+
schema.Properties.Add(
new ProjectionDocumentPropertySchema
{
diff --git a/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..6adf242
--- /dev/null
+++ b/CloudFabric.EAV.Domain/Utilities/Extensions/StringExtensions.cs
@@ -0,0 +1,12 @@
+using System.Text.RegularExpressions;
+
+namespace CloudFabric.EAV.Domain.Utilities.Extensions;
+
+public static class StringExtensions
+{
+ public static string SanitizeForMachineName(this string str)
+ {
+ var specSymbolsRegex = new Regex("[^\\d\\w_]*", RegexOptions.None, TimeSpan.FromMilliseconds(100));
+ return specSymbolsRegex.Replace(str.Replace(" ", "_"), "").ToLower();
+ }
+}
diff --git a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj
index cf7fb5d..4cd4100 100644
--- a/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj
+++ b/CloudFabric.EAV.Models/CloudFabric.EAV.Models.LocalEventSourcingPackages.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs
index 4db28c8..86f1f55 100644
--- a/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs
+++ b/CloudFabric.EAV.Models/RequestModels/CategoryInstanceCreateRequest.cs
@@ -6,6 +6,7 @@ public class CategoryInstanceCreateRequest
public Guid CategoryTreeId { get; set; }
+ public string MachineName { get; set; }
public List Attributes { get; set; }
public Guid? ParentId { get; set; }
diff --git a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs
index 00d6477..1eec2ea 100755
--- a/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs
+++ b/CloudFabric.EAV.Models/RequestModels/EntityInstanceUpdateRequest.cs
@@ -9,3 +9,8 @@ public class EntityInstanceUpdateRequest
public List AttributesToAddOrUpdate { get; set; }
public List? AttributeMachineNamesToRemove { get; set; }
}
+
+public class CategoryUpdateRequest: EntityInstanceUpdateRequest
+{
+
+}
diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs
index 68375bf..22708b3 100644
--- a/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs
+++ b/CloudFabric.EAV.Models/ViewModels/CategoryPathViewModel.cs
@@ -4,4 +4,6 @@ public class CategoryPathViewModel
{
public Guid TreeId { get; set; }
public string Path { get; set; }
+ public Guid? ParentId { get; set; }
+ public string ParentMachineName { get; set; }
}
diff --git a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs
index a1aa458..b224d60 100644
--- a/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs
+++ b/CloudFabric.EAV.Models/ViewModels/CategoryViewModel.cs
@@ -3,8 +3,8 @@ namespace CloudFabric.EAV.Models.ViewModels;
// As the domain model EntityInstanceBase presents a vast array of features
// and represents both EntityInstance and Category with the same properties set,
// it is preferable for one of the models to be inherited from another,
-// in order to avoid code overload and repeats.
+// in order to avoid code overload and repeats.
public class CategoryViewModel : EntityInstanceViewModel
{
-
+ public string MachineName { get; set; }
}
diff --git a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs
index 4f8e156..da8f9f4 100755
--- a/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs
+++ b/CloudFabric.EAV.Models/ViewModels/EntityInstanceViewModel.cs
@@ -21,6 +21,8 @@ public class EntityTreeInstanceViewModel
{
public Guid Id { get; set; }
+ public string MachineName { get; set; }
+
public Guid EntityConfigurationId { get; set; }
public List Attributes { get; set; }
diff --git a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj
index ca82788..f7a353d 100644
--- a/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj
+++ b/CloudFabric.EAV.Service/CloudFabric.EAV.Service.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/CloudFabric.EAV.Service/EAVCategoryService.cs b/CloudFabric.EAV.Service/EAVCategoryService.cs
new file mode 100644
index 0000000..e9cdafa
--- /dev/null
+++ b/CloudFabric.EAV.Service/EAVCategoryService.cs
@@ -0,0 +1,783 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+
+using AutoMapper;
+
+using CloudFabric.EAV.Domain.Models;
+using CloudFabric.EAV.Models.RequestModels;
+using CloudFabric.EAV.Models.ViewModels;
+using CloudFabric.EAV.Options;
+using CloudFabric.EAV.Service.Serialization;
+using CloudFabric.EventSourcing.Domain;
+using CloudFabric.EventSourcing.EventStore;
+using CloudFabric.EventSourcing.EventStore.Persistence;
+using CloudFabric.Projections;
+using CloudFabric.Projections.Queries;
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using ProjectionDocumentSchemaFactory =
+ CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory;
+
+namespace CloudFabric.EAV.Service;
+
+public class EAVCategoryService: EAVService
+{
+
+ private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions;
+
+ public EAVCategoryService(ILogger> logger,
+ IMapper mapper,
+ JsonSerializerOptions jsonSerializerOptions,
+ AggregateRepositoryFactory aggregateRepositoryFactory,
+ ProjectionRepositoryFactory projectionRepositoryFactory,
+ EventUserInfo userInfo,
+ IOptions? elasticSearchQueryOptions = null) : base(logger,
+ new CategoryFromDictionaryDeserializer(mapper),
+ mapper,
+ jsonSerializerOptions,
+ aggregateRepositoryFactory,
+ projectionRepositoryFactory,
+ userInfo)
+ {
+
+ _elasticSearchQueryOptions = elasticSearchQueryOptions != null
+ ? elasticSearchQueryOptions.Value
+ : new ElasticSearchQueryOptions();
+ }
+
+
+ #region Categories
+
+ public async Task<(HierarchyViewModel, ProblemDetails)> CreateCategoryTreeAsync(
+ CategoryTreeCreateRequest entity,
+ Guid? tenantId,
+ CancellationToken cancellationToken = default
+ )
+ {
+ EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync(
+ entity.EntityConfigurationId,
+ entity.EntityConfigurationId.ToString(),
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (entityConfiguration == null)
+ {
+ return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!;
+ }
+
+ var tree = new CategoryTree(
+ Guid.NewGuid(),
+ entity.EntityConfigurationId,
+ entity.MachineName,
+ tenantId
+ );
+
+ _ = await _categoryTreeRepository.SaveAsync(_userInfo, tree, cancellationToken).ConfigureAwait(false);
+ return (_mapper.Map(tree), null)!;
+ }
+
+ ///
+ /// Create new category from provided json string.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4",
+ /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes,
+ /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories
+ /// "parentId" - id guid of category from which new branch of hierarchy will be built.
+ /// Can be null if placed at the root of category tree.
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ ///
+ ///
+ /// (CategoryInstanceCreateRequest createRequest); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ ///
+ ///
+ public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance(
+ string categoryJsonString,
+ Func>? requestDeserializedCallback = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString);
+
+ return CreateCategoryInstance(
+ categoryJson.RootElement,
+ requestDeserializedCallback,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new category from provided json string.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names.
+ /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ ///
+ ///
+ /// id of entity configuration which has all category attributes
+ /// id of category tree, which represents separated hirerarchy with relations between categories
+ /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree.
+ /// tenant id guid. A guid which uniquely identifies and isolates the data. For single
+ /// tenant application this should be one hardcoded guid for whole app.
+ ///
+ /// (CategoryInstanceCreateRequest createRequest); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ ///
+ ///
+ public Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance(
+ string categoryJsonString,
+ string machineName,
+ Guid categoryConfigurationId,
+ Guid categoryTreeId,
+ Guid? parentId,
+ Guid? tenantId,
+ Func>? requestDeserializedCallback = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ JsonDocument categoryJson = JsonDocument.Parse(categoryJsonString);
+
+ return CreateCategoryInstance(
+ categoryJson.RootElement,
+ machineName,
+ categoryConfigurationId,
+ categoryTreeId,
+ parentId,
+ tenantId,
+ requestDeserializedCallback,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new category from provided json document.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4",
+ /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes,
+ /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories
+ /// "parentId" - id guid of category from which new branch of hierarchy will be built.
+ /// Can be null if placed at the root of category tree.
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ ///
+ ///
+ /// (CategoryInstanceCreateRequest createRequest); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ ///
+ ///
+ public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance(
+ JsonElement categoryJson,
+ Func>? requestDeserializedCallback = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var (categoryInstanceCreateRequest, deserializationErrors) =
+ await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, cancellationToken: cancellationToken);
+
+ if (deserializationErrors != null)
+ {
+ return (null, deserializationErrors);
+ }
+
+ return await CreateCategoryInstance(
+ categoryJson,
+ categoryInstanceCreateRequest!.MachineName,
+ categoryInstanceCreateRequest!.CategoryConfigurationId,
+ categoryInstanceCreateRequest.CategoryTreeId,
+ categoryInstanceCreateRequest.ParentId,
+ categoryInstanceCreateRequest.TenantId,
+ requestDeserializedCallback,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new category from provided json document.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names.
+ /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ ///
+ /// id of entity configuration which has all category attributes
+ /// id of category tree, which represents separated hirerarchy with relations between categories
+ /// id of category from which new branch of hierarchy will be built. Can be null if placed at the root of category tree.
+ /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single
+ /// tenant application this should be one hardcoded guid for whole app.
+ ///
+ /// (CategoryInstanceCreateRequest createRequest); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to CategoryInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ ///
+ ///
+ public async Task<(JsonDocument?, ProblemDetails?)> CreateCategoryInstance(
+ JsonElement categoryJson,
+ string machineName,
+ Guid categoryConfigurationId,
+ Guid categoryTreeId,
+ Guid? parentId,
+ Guid? tenantId,
+ Func>? requestDeserializedCallback = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ (CategoryInstanceCreateRequest? categoryInstanceCreateRequest, ProblemDetails? deserializationErrors)
+ = await DeserializeCategoryInstanceCreateRequestFromJson(
+ categoryJson,
+ machineName,
+ categoryConfigurationId,
+ categoryTreeId,
+ parentId,
+ tenantId,
+ cancellationToken
+ );
+
+ if (deserializationErrors != null)
+ {
+ return (null, deserializationErrors);
+ }
+
+ if (requestDeserializedCallback != null)
+ {
+ categoryInstanceCreateRequest = await requestDeserializedCallback(categoryInstanceCreateRequest!);
+ }
+
+ var (createdCategory, validationErrors) = await CreateCategoryInstance(
+ categoryInstanceCreateRequest!, cancellationToken
+ );
+
+ if (validationErrors != null)
+ {
+ return (null, validationErrors);
+ }
+
+ return (SerializeEntityInstanceToJsonMultiLanguage(_mapper.Map(createdCategory)), null);
+ }
+
+ public async Task<(CategoryViewModel, ProblemDetails)> CreateCategoryInstance(
+ CategoryInstanceCreateRequest categoryCreateRequest,
+ CancellationToken cancellationToken = default
+ )
+ {
+ CategoryTree? tree = await _categoryTreeRepository.LoadAsync(
+ categoryCreateRequest.CategoryTreeId,
+ categoryCreateRequest.CategoryTreeId.ToString(),
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (tree == null)
+ {
+ return (null, new ValidationErrorResponse("CategoryTreeId", "Category tree not found"))!;
+ }
+
+ if (tree.EntityConfigurationId != categoryCreateRequest.CategoryConfigurationId)
+ {
+ return (null,
+ new ValidationErrorResponse("CategoryConfigurationId",
+ "Category tree uses another configuration for categories"
+ ))!;
+ }
+
+ EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync(
+ categoryCreateRequest.CategoryConfigurationId,
+ categoryCreateRequest.CategoryConfigurationId.ToString(),
+ cancellationToken
+ ).ConfigureAwait(false);
+
+
+ if (entityConfiguration == null)
+ {
+ return (null, new ValidationErrorResponse("CategoryConfigurationId", "Configuration not found"))!;
+ }
+
+ List attributeConfigurations =
+ await GetAttributeConfigurationsForEntityConfiguration(
+ entityConfiguration,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+
+ (var categoryPath, Guid? parentId, ProblemDetails? errors) =
+ await BuildCategoryPath(tree.Id, categoryCreateRequest.ParentId, cancellationToken).ConfigureAwait(false);
+
+ if (errors != null)
+ {
+ return (null, errors)!;
+ }
+
+ var categoryInstance = new Category(
+ Guid.NewGuid(),
+ categoryCreateRequest.MachineName,
+ categoryCreateRequest.CategoryConfigurationId,
+ _mapper.Map>(categoryCreateRequest.Attributes),
+ categoryCreateRequest.TenantId,
+ categoryPath!,
+ parentId,
+ categoryCreateRequest.CategoryTreeId
+ );
+
+ var validationErrors = new Dictionary();
+ foreach (AttributeConfiguration a in attributeConfigurations)
+ {
+ AttributeInstance? attributeValue = categoryInstance.Attributes
+ .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName);
+
+ List attrValidationErrors = a.ValidateInstance(attributeValue);
+ if (attrValidationErrors is { Count: > 0 })
+ {
+ validationErrors.Add(a.MachineName, attrValidationErrors.ToArray());
+ }
+ }
+
+ if (validationErrors.Count > 0)
+ {
+ return (null, new ValidationErrorResponse(validationErrors))!;
+ }
+
+
+
+ var saved = await _categoryInstanceRepository.SaveAsync(_userInfo, categoryInstance, cancellationToken)
+ .ConfigureAwait(false);
+ if (!saved)
+ {
+ //TODO: What do we want to do with internal exceptions and unsuccessful flow?
+ throw new Exception("Entity was not saved");
+ }
+
+ return (_mapper.Map(categoryInstance), null)!;
+ }
+
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "categoryTreeId": "65053391-9f0e-4b86-959e-2fe342e705d4",
+ /// "parentId": "3e302832-ce6b-4c41-9cf8-e2b3fdd7b01c",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all category attributes,
+ /// "categoryTreeId" - guid of category tree, which represents separated hirerarchy with relations between categories
+ /// "parentId" - id guid of category from which new branch of hierarchy will be built.
+ /// Can be null if placed at the root of category tree.
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson(
+ JsonElement categoryJson,
+ CancellationToken cancellationToken = default
+ )
+ {
+ Guid categoryConfigurationId;
+ if (categoryJson.TryGetProperty("categoryConfigurationId", out var categoryConfigurationIdJsonElement))
+ {
+ if (categoryConfigurationIdJsonElement.TryGetGuid(out var categoryConfigurationIdGuid))
+ {
+ categoryConfigurationId = categoryConfigurationIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is not a valid Guid"))!;
+ }
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("categoryConfigurationId", "Value is missing"));
+ }
+
+ Guid categoryTreeId;
+ if (categoryJson.TryGetProperty("categoryTreeId", out var categoryTreeIdJsonElement))
+ {
+ if (categoryTreeIdJsonElement.TryGetGuid(out var categoryTreeIdGuid))
+ {
+ categoryTreeId = categoryTreeIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("categoryTreeId", "Value is not a valid Guid"))!;
+ }
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("categoryTreeId", "Value is missing"));
+ }
+
+ Guid? parentId = null;
+ if (categoryJson.TryGetProperty("parentId", out var parentIdJsonElement))
+ {
+ if (parentIdJsonElement.ValueKind == JsonValueKind.Null)
+ {
+ parentId = null;
+ }
+ else if (parentIdJsonElement.TryGetGuid(out var parentIdGuid))
+ {
+ parentId = parentIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("parentId", "Value is not a valid Guid"))!;
+ }
+ }
+
+ Guid? tenantId = null;
+ if (categoryJson.TryGetProperty("tenantId", out var tenantIdJsonElement))
+ {
+ if (tenantIdJsonElement.ValueKind == JsonValueKind.Null)
+ {
+ tenantId = null;
+ }
+ else if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid))
+ {
+ tenantId = tenantIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!;
+ }
+ }
+
+ string? machineName = null;
+ if (categoryJson.TryGetProperty("machineName", out var machineNameJsonElement))
+ {
+ machineName = machineNameJsonElement.ValueKind == JsonValueKind.Null ? null : machineNameJsonElement.GetString();
+ if (machineName == null)
+ {
+ return (null, new ValidationErrorResponse("machineName", "Value is not a valid"));
+ }
+ }
+
+ return await DeserializeCategoryInstanceCreateRequestFromJson(categoryJson, machineName!, categoryConfigurationId, categoryTreeId, parentId, tenantId, cancellationToken);
+ }
+
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "name": "Main Category",
+ /// "desprition": "Main Category description"
+ /// }
+ /// ```
+ ///
+ /// Where "name" and "description" are attributes machine names.
+ /// Note that this overload accepts "entityConfigurationId", "categoryTreeId", "parentId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ public async Task<(CategoryInstanceCreateRequest?, ProblemDetails?)> DeserializeCategoryInstanceCreateRequestFromJson(
+ JsonElement categoryJson,
+ string machineName,
+ Guid categoryConfigurationId,
+ Guid categoryTreeId,
+ Guid? parentId,
+ Guid? tenantId,
+ CancellationToken cancellationToken = default
+ )
+ {
+ EntityConfiguration? categoryConfiguration = await _entityConfigurationRepository.LoadAsync(
+ categoryConfigurationId,
+ categoryConfigurationId.ToString(),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ if (categoryConfiguration == null)
+ {
+ return (null, new ValidationErrorResponse("CategoryConfigurationId", "CategoryConfiguration not found"))!;
+ }
+
+ List attributeConfigurations = await GetAttributeConfigurationsForEntityConfiguration(
+ categoryConfiguration,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeCategoryInstanceCreateRequest(
+ categoryConfigurationId, machineName, tenantId, categoryTreeId, parentId, attributeConfigurations, categoryJson
+ );
+ }
+
+ ///
+ /// Returns full category tree.
+ /// If notDeeperThanCategoryId is specified - returns category tree with all categories that are above or on the same lavel as a provided.
+ ///
+ ///
+ ///
+ ///
+
+ [SuppressMessage("Performance", "CA1806:Do not ignore method results")]
+ public async Task> GetCategoryTreeViewAsync(
+ Guid treeId,
+ Guid? notDeeperThanCategoryId = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ CategoryTree? tree = await _categoryTreeRepository.LoadAsync(treeId, treeId.ToString(), cancellationToken)
+ .ConfigureAwait(false);
+ if (tree == null)
+ {
+ throw new NotFoundException("Category tree not found");
+ }
+
+ ProjectionQueryResult treeElementsQueryResult =
+ await QueryInstances(tree.EntityConfigurationId,
+ new ProjectionQuery
+ {
+ Filters = new List { new("CategoryPaths.TreeId", FilterOperator.Equal, treeId) },
+ Limit = _elasticSearchQueryOptions.MaxSize
+ },
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ var treeElements = treeElementsQueryResult.Records
+ .Select(x => x.Document!)
+ .Select(x =>
+ {
+ x.CategoryPaths = x.CategoryPaths.Where(cp => cp.TreeId == treeId).ToList();
+ return x;
+ }).ToList();
+
+
+ return BuildTreeView(treeElements, notDeeperThanCategoryId);
+
+ }
+
+ private List BuildTreeView(List categories, Guid? notDeeperThanCategoryId)
+ {
+
+ int searchedLevelPathLenght;
+
+ if (notDeeperThanCategoryId != null)
+ {
+ var category = categories.FirstOrDefault(x => x.Id == notDeeperThanCategoryId);
+
+ if (category == null)
+ {
+ throw new NotFoundException("Category not found");
+ }
+
+ searchedLevelPathLenght = category.CategoryPaths.FirstOrDefault()!.Path.Length;
+
+ categories = categories
+ .Where(x => x.CategoryPaths.FirstOrDefault()!.Path.Length <= searchedLevelPathLenght).ToList();
+ }
+
+ var treeViewModel = new List();
+
+ // Go through each instance once
+ foreach (CategoryViewModel treeElement in categories
+ .OrderBy(x => x.CategoryPaths.FirstOrDefault()?.Path.Length))
+ {
+ 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
+ if (string.IsNullOrEmpty(categoryPath))
+ {
+ treeViewModel.Add(treeElementViewModel);
+ }
+ else
+ {
+ // Else split categoryPath and extract each parent machine name
+ IEnumerable categoryPathElements =
+ categoryPath.Split('/').Where(x => !string.IsNullOrEmpty(x));
+
+ // Go through each element of the path, remembering where we are atm, and passing current version of treeViewModel
+ // Applies an accumulator function over a sequence of paths.
+ EntityTreeInstanceViewModel? currentLevel = null;
+
+ categoryPathElements.Aggregate(
+ treeViewModel, // initial value
+ (treeViewModelCurrent, pathComponent) => // apply function to a sequence
+ {
+ // try to find parent with current pathComponent in the current version of treeViewModel in case
+ // it had already been added to our tree model on previous iterations
+ EntityTreeInstanceViewModel? parent =
+ treeViewModelCurrent.FirstOrDefault(y => y.MachineName == pathComponent);
+
+ // If it is not still there -> find it in the global list of categories and add to our treeViewModel
+ if (parent == null)
+ {
+ CategoryViewModel? parentInstance = categories.FirstOrDefault(y => y.MachineName == pathComponent);
+ parent = _mapper.Map(parentInstance);
+ treeViewModelCurrent.Add(parent);
+ }
+
+ // Move to the next level
+ currentLevel = parent;
+ return parent.Children;
+ }
+ );
+ currentLevel?.Children.Add(treeElementViewModel);
+ }
+ }
+ return treeViewModel;
+
+ }
+
+ ///
+ /// Returns children at one level below of the parent category in internal CategoryParentChildrenViewModel format.
+ ///
+ ///
+ ///
+ ///
+ public async Task> GetSubcategories(
+ Guid categoryTreeId,
+ Guid? parentId = null,
+ string? parentMachineName = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var categoryTree = await _categoryTreeRepository.LoadAsync(
+ categoryTreeId, categoryTreeId.ToString(), cancellationToken
+ ).ConfigureAwait(false);
+
+ if (categoryTree == null)
+ {
+ throw new NotFoundException("Category tree not found");
+ }
+
+ var query = GetSubcategoriesPrepareQuery(categoryTree, parentId, parentMachineName, cancellationToken);
+
+ var queryResult = _mapper.Map>(
+ await QueryInstances(categoryTree.EntityConfigurationId, query, cancellationToken)
+ );
+
+ return queryResult.Records.Select(x => x.Document).ToList() ?? new List();
+ }
+
+ private ProjectionQuery GetSubcategoriesPrepareQuery(
+ CategoryTree categoryTree,
+ Guid? parentId,
+ string? parentMachineName,
+ CancellationToken cancellationToken = default
+ )
+ {
+ ProjectionQuery query = new ProjectionQuery
+ {
+ Limit = _elasticSearchQueryOptions.MaxSize
+ };
+
+ query.Filters.Add(new Filter
+ {
+ PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.TreeId)}",
+ Operator = FilterOperator.Equal,
+ Value = categoryTree.Id.ToString(),
+ });
+
+ // If nothing is set - get subcategories of master level
+ if (parentId == null && string.IsNullOrEmpty(parentMachineName))
+ {
+ query.Filters.Add(new Filter
+ {
+ PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}",
+ Operator = FilterOperator.Equal,
+ Value = string.Empty,
+ });
+ return query;
+ }
+
+ if (parentId != null)
+ {
+
+ query.Filters.Add(new Filter
+ {
+ PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentId)}",
+ Operator = FilterOperator.Equal,
+ Value = parentId.ToString()
+ });
+ }
+
+ if (!string.IsNullOrEmpty(parentMachineName))
+ {
+
+ query.Filters.Add(new Filter
+ {
+ PropertyName = $"{nameof(CategoryViewModel.CategoryPaths)}.{nameof(CategoryPath.ParentMachineName)}",
+ Operator = FilterOperator.Equal,
+ Value = parentMachineName
+ });
+ }
+ return query;
+
+ }
+
+ #endregion
+
+}
diff --git a/CloudFabric.EAV.Service/EAVEntityInstanceService.cs b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs
new file mode 100644
index 0000000..2ceab7c
--- /dev/null
+++ b/CloudFabric.EAV.Service/EAVEntityInstanceService.cs
@@ -0,0 +1,477 @@
+using System.Text.Json;
+
+using AutoMapper;
+
+using CloudFabric.EAV.Domain.Models;
+using CloudFabric.EAV.Domain.Models.Attributes;
+using CloudFabric.EAV.Enums;
+using CloudFabric.EAV.Models.RequestModels;
+using CloudFabric.EAV.Models.ViewModels;
+using CloudFabric.EAV.Options;
+using CloudFabric.EAV.Service.Serialization;
+using CloudFabric.EventSourcing.Domain;
+using CloudFabric.EventSourcing.EventStore;
+using CloudFabric.EventSourcing.EventStore.Persistence;
+using CloudFabric.Projections;
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using ProjectionDocumentSchemaFactory =
+ CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory;
+namespace CloudFabric.EAV.Service;
+
+public class EAVEntityInstanceService: EAVService
+{
+
+ public EAVEntityInstanceService(ILogger> logger,
+ IMapper mapper,
+ JsonSerializerOptions jsonSerializerOptions,
+ AggregateRepositoryFactory aggregateRepositoryFactory,
+ ProjectionRepositoryFactory projectionRepositoryFactory,
+ EventUserInfo userInfo) : base(logger,
+ new EntityInstanceFromDictionaryDeserializer(mapper),
+ mapper,
+ jsonSerializerOptions,
+ aggregateRepositoryFactory,
+ projectionRepositoryFactory,
+ userInfo)
+ {
+ }
+
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "sku" and "name" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes,
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson(
+ JsonElement entityJson,
+ CancellationToken cancellationToken = default
+ )
+ {
+ Guid entityConfigurationId;
+ if (entityJson.TryGetProperty("entityConfigurationId", out var entityConfigurationIdJsonElement))
+ {
+ if (entityConfigurationIdJsonElement.TryGetGuid(out var entityConfigurationIdGuid))
+ {
+ entityConfigurationId = entityConfigurationIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("entityConfigurationId", "Value is not a valid Guid"))!;
+ }
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("entityConfigurationId", "Value is missing"));
+ }
+
+ Guid tenantId;
+ if (entityJson.TryGetProperty("tenantId", out var tenantIdJsonElement))
+ {
+ if (tenantIdJsonElement.TryGetGuid(out var tenantIdGuid))
+ {
+ tenantId = tenantIdGuid;
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("tenantId", "Value is not a valid Guid"))!;
+ }
+ }
+ else
+ {
+ return (null, new ValidationErrorResponse("tenantId", "Value is missing"));
+ }
+
+ return await DeserializeEntityInstanceCreateRequestFromJson(
+ entityJson, entityConfigurationId, tenantId, cancellationToken
+ );
+ }
+
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity"
+ /// }
+ /// ```
+ ///
+ /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ public async Task<(EntityInstanceCreateRequest?, ProblemDetails?)> DeserializeEntityInstanceCreateRequestFromJson(
+ JsonElement entityJson,
+ Guid entityConfigurationId,
+ Guid tenantId,
+ CancellationToken cancellationToken = default
+ )
+ {
+ EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync(
+ entityConfigurationId,
+ entityConfigurationId.ToString(),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ if (entityConfiguration == null)
+ {
+ return (null, new ValidationErrorResponse("EntityConfigurationId", "EntityConfiguration not found"))!;
+ }
+
+ List attributeConfigurations =
+ await GetAttributeConfigurationsForEntityConfiguration(
+ entityConfiguration,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return await _entityInstanceCreateUpdateRequestFromJsonDeserializer.DeserializeEntityInstanceCreateRequest(
+ entityConfigurationId, tenantId, attributeConfigurations, entityJson
+ );
+ }
+
+ ///
+ /// Create new entity instance from provided json string.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "sku" and "name" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes,
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ ///
+ ///
+ /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ /// Note that it's important to check dryRun parameter and not make any changes to persistent store if
+ /// the parameter equals to 'true'.
+ ///
+ /// If true, entity will only be validated but not saved to the database
+ ///
+ ///
+ public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance(
+ string entityJsonString,
+ Func>? requestDeserializedCallback = null,
+ bool dryRun = false,
+ bool requiredAttributesCanBeNull = false,
+ CancellationToken cancellationToken = default
+ )
+ {
+ JsonDocument entityJson = JsonDocument.Parse(entityJsonString);
+
+ return CreateEntityInstance(
+ entityJson.RootElement,
+ requestDeserializedCallback,
+ dryRun,
+ requiredAttributesCanBeNull,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new entity instance from provided json string.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity"
+ /// }
+ /// ```
+ ///
+ /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ ///
+ /// Id of entity configuration which has all attributes
+ /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single
+ /// tenant application this should be one hardcoded guid for whole app.
+ ///
+ /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ /// Note that it's important to check dryRun parameter and not make any changes to persistent store if
+ /// the parameter equals to 'true'.
+ ///
+ /// If true, entity will only be validated but not saved to the database
+ ///
+ ///
+ public Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance(
+ string entityJsonString,
+ Guid entityConfigurationId,
+ Guid tenantId,
+ Func>? requestDeserializedCallback = null,
+ bool dryRun = false,
+ bool requiredAttributesCanBeNull = false,
+ CancellationToken cancellationToken = default
+ )
+ {
+ JsonDocument entityJson = JsonDocument.Parse(entityJsonString);
+
+ return CreateEntityInstance(
+ entityJson.RootElement,
+ entityConfigurationId,
+ tenantId,
+ requestDeserializedCallback,
+ dryRun,
+ requiredAttributesCanBeNull,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new entity instance from provided json document.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity",
+ /// "entityConfigurationId": "fb80cb74-6f47-4d38-bb87-25bd820efee7",
+ /// "tenantId": "b6842a71-162b-411d-86e9-3ec01f909c82"
+ /// }
+ /// ```
+ ///
+ /// Where "sku" and "name" are attributes machine names,
+ /// "entityConfigurationId" - obviously the id of entity configuration which has all attributes,
+ /// "tenantId" - tenant id guid. A guid which uniquely identifies and isolates the data. For single tenant
+ /// application this should be one hardcoded guid for whole app.
+ ///
+ ///
+ ///
+ ///
+ /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ /// Note that it's important to check dryRun parameter and not make any changes to persistent store if
+ /// the parameter equals to 'true'.
+ ///
+ /// If true, entity will only be validated but not saved to the database
+ ///
+ ///
+ public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance(
+ JsonElement entityJson,
+ Func>? requestDeserializedCallback = null,
+ bool dryRun = false,
+ bool requiredAttributesCanBeNull = false,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var (entityInstanceCreateRequest, deserializationErrors) =
+ await DeserializeEntityInstanceCreateRequestFromJson(entityJson, cancellationToken);
+
+ if (deserializationErrors != null)
+ {
+ return (null, deserializationErrors);
+ }
+
+ return await CreateEntityInstance(
+ entityJson,
+ // Deserialization method ensures that EntityConfigurationId and TenantId exist and returns errors if not
+ // so it's safe to use ! here
+ entityInstanceCreateRequest!.EntityConfigurationId,
+ entityInstanceCreateRequest.TenantId!.Value,
+ requestDeserializedCallback,
+ dryRun,
+ requiredAttributesCanBeNull,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Create new entity instance from provided json document.
+ ///
+ ///
+ /// Use following json format:
+ ///
+ /// ```
+ /// {
+ /// "sku": "123",
+ /// "name": "New Entity"
+ /// }
+ /// ```
+ ///
+ /// Note that this overload accepts "entityConfigurationId" and "tenantId" via method arguments,
+ /// so they should not be in json.
+ ///
+ ///
+ ///
+ /// Id of entity configuration which has all attributes
+ /// Tenant id guid. A guid which uniquely identifies and isolates the data. For single
+ /// tenant application this should be one hardcoded guid for whole app.
+ ///
+ /// (EntityInstanceCreateRequest createRequest, bool dryRun); ]]>
+ ///
+ /// This function will be called after deserializing the request from json
+ /// to EntityInstanceCreateRequest and allows adding additional validation or any other pre-processing logic.
+ ///
+ /// Note that it's important to check dryRun parameter and not make any changes to persistent store if
+ /// the parameter equals to 'true'.
+ ///
+ /// If true, entity will only be validated but not saved to the database
+ ///
+ ///
+ public async Task<(JsonDocument?, ProblemDetails?)> CreateEntityInstance(
+ JsonElement entityJson,
+ Guid entityConfigurationId,
+ Guid tenantId,
+ Func>? requestDeserializedCallback = null,
+ bool dryRun = false,
+ bool requiredAttributesCanBeNull = false,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var (entityInstanceCreateRequest, deserializationErrors) = await
+ DeserializeEntityInstanceCreateRequestFromJson(
+ entityJson, entityConfigurationId, tenantId, cancellationToken
+ );
+
+ if (deserializationErrors != null)
+ {
+ return (null, deserializationErrors);
+ }
+
+ if (requestDeserializedCallback != null)
+ {
+ entityInstanceCreateRequest = await requestDeserializedCallback(entityInstanceCreateRequest!, dryRun);
+ }
+
+ var (createdEntity, validationErrors) = await CreateEntityInstance(
+ entityInstanceCreateRequest!, dryRun, requiredAttributesCanBeNull, cancellationToken
+ );
+
+ if (validationErrors != null)
+ {
+ return (null, validationErrors);
+ }
+
+ return (SerializeEntityInstanceToJsonMultiLanguage(createdEntity), null);
+ }
+
+
+
+ public async Task<(EntityInstanceViewModel?, ProblemDetails?)> CreateEntityInstance(
+ EntityInstanceCreateRequest entity, bool dryRun = false, bool requiredAttributesCanBeNull = false, CancellationToken cancellationToken = default
+ )
+ {
+ EntityConfiguration? entityConfiguration = await _entityConfigurationRepository.LoadAsync(
+ entity.EntityConfigurationId,
+ entity.EntityConfigurationId.ToString(),
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ if (entityConfiguration == null)
+ {
+ return (null, new ValidationErrorResponse("EntityConfigurationId", "Configuration not found"))!;
+ }
+
+ List attributeConfigurations =
+ await GetAttributeConfigurationsForEntityConfiguration(
+ entityConfiguration,
+ cancellationToken
+ ).ConfigureAwait(false);
+
+ //TODO: add check for categoryPath
+ var entityInstance = new EntityInstance(
+ Guid.NewGuid(),
+ entity.EntityConfigurationId,
+ _mapper.Map>(entity.Attributes),
+ entity.TenantId
+ );
+
+ var validationErrors = new Dictionary();
+ foreach (AttributeConfiguration a in attributeConfigurations)
+ {
+ AttributeInstance? attributeValue = entityInstance.Attributes
+ .FirstOrDefault(attr => a.MachineName == attr.ConfigurationAttributeMachineName);
+
+ List attrValidationErrors = a.ValidateInstance(attributeValue, requiredAttributesCanBeNull);
+ if (attrValidationErrors is { Count: > 0 })
+ {
+ validationErrors.Add(a.MachineName, attrValidationErrors.ToArray());
+ }
+
+ // Note that this method updates entityConfiguration state (for serial attribute it increments the number
+ // stored in externalvalues) but does not save entity configuration, we need to do that manually outside of
+ // the loop
+ InitializeAttributeInstanceWithExternalValuesFromEntity(entityConfiguration, a, attributeValue);
+ }
+
+ if (validationErrors.Count > 0)
+ {
+ return (null, new ValidationErrorResponse(validationErrors))!;
+ }
+
+ if (!dryRun)
+ {
+ var entityConfigurationSaved = await _entityConfigurationRepository
+ .SaveAsync(_userInfo, entityConfiguration, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (!entityConfigurationSaved)
+ {
+ throw new Exception("Entity was not saved");
+ }
+
+ ProjectionDocumentSchema schema = ProjectionDocumentSchemaFactory
+ .FromEntityConfiguration(entityConfiguration, attributeConfigurations);
+
+ IProjectionRepository projectionRepository = _projectionRepositoryFactory.GetProjectionRepository(schema);
+ await projectionRepository.EnsureIndex(cancellationToken).ConfigureAwait(false);
+
+ var entityInstanceSaved =
+ await _entityInstanceRepository.SaveAsync(_userInfo, entityInstance, cancellationToken);
+
+ if (!entityInstanceSaved)
+ {
+ //TODO: What do we want to do with internal exceptions and unsuccessful flow?
+ throw new Exception("Entity was not saved");
+ }
+
+ return (_mapper.Map(entityInstance), null);
+ }
+
+ return (_mapper.Map(entityInstance), null);
+ }
+}
diff --git a/CloudFabric.EAV.Service/EAVService.cs b/CloudFabric.EAV.Service/EAVService.cs
index 779146b..a1fbbb0 100644
--- a/CloudFabric.EAV.Service/EAVService.cs
+++ b/CloudFabric.EAV.Service/EAVService.cs
@@ -15,7 +15,6 @@
using CloudFabric.EAV.Models.RequestModels.Attributes;
using CloudFabric.EAV.Models.ViewModels;
using CloudFabric.EAV.Models.ViewModels.Attributes;
-using CloudFabric.EAV.Options;
using CloudFabric.EAV.Service.Serialization;
using CloudFabric.EventSourcing.Domain;
using CloudFabric.EventSourcing.EventStore;
@@ -25,85 +24,86 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using ProjectionDocumentSchemaFactory =
CloudFabric.EAV.Domain.Projections.EntityInstanceProjection.ProjectionDocumentSchemaFactory;
namespace CloudFabric.EAV.Service;
-public class EAVService : IEAVService
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+public abstract class EAVService where TViewModel: EntityInstanceViewModel
+ where TUpdateRequest: EntityInstanceUpdateRequest
+ where TEntityType: EntityInstanceBase
{
- private readonly AggregateRepositoryFactory _aggregateRepositoryFactory;
-
private readonly IProjectionRepository
_attributeConfigurationProjectionRepository;
private readonly AggregateRepository _attributeConfigurationRepository;
- private readonly AggregateRepository _categoryTreeRepository;
private readonly IProjectionRepository
_entityConfigurationProjectionRepository;
- private readonly AggregateRepository _entityConfigurationRepository;
+ internal readonly AggregateRepository _entityConfigurationRepository;
- private readonly EntityInstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer;
+ private readonly InstanceFromDictionaryDeserializer _entityInstanceFromDictionaryDeserializer;
- private readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer
+ internal readonly EntityInstanceCreateUpdateRequestFromJsonDeserializer
_entityInstanceCreateUpdateRequestFromJsonDeserializer;
- private readonly AggregateRepository _entityInstanceRepository;
- private readonly ILogger _logger;
- private readonly IMapper _mapper;
+ internal readonly AggregateRepository _entityInstanceRepository;
+ private readonly ILogger> _logger;
+ internal readonly IMapper _mapper;
private readonly JsonSerializerOptions _jsonSerializerOptions;
- private readonly ProjectionRepositoryFactory _projectionRepositoryFactory;
+ internal readonly ProjectionRepositoryFactory _projectionRepositoryFactory;
- private readonly EventUserInfo _userInfo;
+ internal readonly EventUserInfo _userInfo;
- private readonly ElasticSearchQueryOptions _elasticSearchQueryOptions;
+ internal readonly AggregateRepository _categoryTreeRepository;
+ internal readonly AggregateRepository _categoryInstanceRepository;
- public EAVService(
- ILogger logger,
+ protected EAVService(
+ ILogger> logger,
+ InstanceFromDictionaryDeserializer instanceFromDictionaryDeserializer,
IMapper mapper,
JsonSerializerOptions jsonSerializerOptions,
AggregateRepositoryFactory aggregateRepositoryFactory,
ProjectionRepositoryFactory projectionRepositoryFactory,
- EventUserInfo userInfo,
- IOptions? elasticSearchQueryOptions = null
+ EventUserInfo userInfo
)
{
_logger = logger;
_mapper = mapper;
_jsonSerializerOptions = jsonSerializerOptions;
- _aggregateRepositoryFactory = aggregateRepositoryFactory;
_projectionRepositoryFactory = projectionRepositoryFactory;
_userInfo = userInfo;
- _elasticSearchQueryOptions = elasticSearchQueryOptions != null
- ? elasticSearchQueryOptions.Value
- : new ElasticSearchQueryOptions();
- _attributeConfigurationRepository = _aggregateRepositoryFactory
+
+ _attributeConfigurationRepository = aggregateRepositoryFactory
.GetAggregateRepository();
- _entityConfigurationRepository = _aggregateRepositoryFactory
+ _entityConfigurationRepository = aggregateRepositoryFactory
.GetAggregateRepository();
- _entityInstanceRepository = _aggregateRepositoryFactory
- .GetAggregateRepository();
- _categoryTreeRepository = _aggregateRepositoryFactory
- .GetAggregateRepository();
+ _entityInstanceRepository = aggregateRepositoryFactory
+ .GetAggregateRepository();
+
_attributeConfigurationProjectionRepository = _projectionRepositoryFactory
.GetProjectionRepository();
_entityConfigurationProjectionRepository = _projectionRepositoryFactory
.GetProjectionRepository();
- _entityInstanceFromDictionaryDeserializer = new EntityInstanceFromDictionaryDeserializer(_mapper);
+ _entityInstanceFromDictionaryDeserializer = instanceFromDictionaryDeserializer;
_entityInstanceCreateUpdateRequestFromJsonDeserializer =
new EntityInstanceCreateUpdateRequestFromJsonDeserializer(
_attributeConfigurationRepository, jsonSerializerOptions
);
+
+ _categoryInstanceRepository = aggregateRepositoryFactory
+ .GetAggregateRepository();
+ _categoryTreeRepository = aggregateRepositoryFactory
+ .GetAggregateRepository();
}
private void EnsureAttributeMachineNameIsAdded(AttributeConfigurationCreateUpdateRequest attributeRequest)
@@ -157,7 +157,7 @@ private async Task CheckAttributesListMachineNameUnique(
.Select(x => ((EntityAttributeConfigurationCreateUpdateReferenceRequest)x).AttributeConfigurationId)
.ToList();
- List machineNames = new();
+ List machineNames = new List();
if (referenceAttributes.Any())
{
machineNames = (await GetAttributesByIds(referenceAttributes, cancellationToken))
@@ -185,61 +185,7 @@ private async Task CheckAttributesListMachineNameUnique(
return true;
}
- private void InitializeAttributeInstanceWithExternalValuesFromEntity(
- EntityConfiguration entityConfiguration,
- AttributeConfiguration attributeConfiguration,
- AttributeInstance? attributeInstance
- )
- {
- switch (attributeConfiguration.ValueType)
- {
- case EavAttributeType.Serial:
- {
- if (attributeInstance == null)
- {
- return;
- }
-
- var serialAttributeConfiguration = attributeConfiguration as SerialAttributeConfiguration;
-
- var serialInstance = attributeInstance as SerialAttributeInstance;
-
- if (serialAttributeConfiguration == null || serialInstance == null)
- {
- throw new ArgumentException("Invalid attribute type");
- }
-
- EntityConfigurationAttributeReference? entityAttribute = entityConfiguration.Attributes
- .FirstOrDefault(x => x.AttributeConfigurationId == attributeConfiguration.Id);
-
- if (entityAttribute == null)
- {
- throw new NotFoundException("Attribute not found");
- }
-
- var existingAttributeValue =
- entityAttribute.AttributeConfigurationExternalValues.FirstOrDefault();
-
- long? deserializedValue = null;
-
- if (existingAttributeValue != null)
- {
- deserializedValue = JsonSerializer.Deserialize(existingAttributeValue.ToString()!);
- }
-
- var newExternalValue = existingAttributeValue == null
- ? serialAttributeConfiguration.StartingNumber
- : deserializedValue += serialAttributeConfiguration.Increment;
-
- serialInstance.Value = newExternalValue!.Value;
- entityConfiguration.UpdateAttrributeExternalValues(attributeConfiguration.Id,
- new List