Repository: VirtoCommerce/vc-module-x-catalog
Pull request #44: VCST-2688: add property group field
Author: login: ksavosteev
Created: 2025-04-02T12:54:35Z | Merged: 2025-04-28T14:58:14Z

## Description
Added `Group field` of type `PropertyGroupType` to `PropertyType`:

```graphql
query products {
  products(
    first:20
    storeId: "Electronics", 
    currencyCode: "EUR", 
    cultureName: "de-DE",
  )
  {
    totalCount
    items {     
     id
     code
     name
     properties {
      name
      group {
        id
        name
        displayOrder
        description
        }
      }
    }
  }
}
```

## References
### QA-test:
### Jira-link:









https://virtocommerce.atlassian.net/browse/VCST-2688
### Artifact URL:
https://vc3prerelease.blob.core.windows.net/packages/VirtoCommerce.XCatalog_3.914.0-pr-44-7848.zip


Diff:
diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyGroupType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyGroupType.cs
new file mode 100644
index 0000000..934d5f6
--- /dev/null
+++ b/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyGroupType.cs
@@ -0,0 +1,45 @@
+using GraphQL;
+using GraphQL.Types;
+using VirtoCommerce.CatalogModule.Core.Model;
+using VirtoCommerce.Platform.Core.Common;
+using VirtoCommerce.Xapi.Core.Extensions;
+using VirtoCommerce.Xapi.Core.Schemas;
+
+namespace VirtoCommerce.XCatalog.Core.Schemas
+{
+    public class PropertyGroupType : ExtendableGraphType<PropertyGroup>
+    {
+        public PropertyGroupType()
+        {
+            Name = "PropertyGroup";
+            Description = "Property group.";
+
+            Field(x => x.Id, nullable: false).Description("The unique ID of the property group.");
+            Field(x => x.DisplayOrder, nullable: true).Description("The display order of the property group.");
+
+            Field<StringGraphType>("name")
+                .Resolve(context => GetLocalizedValue(context, context.Source.LocalizedName, context.Source.Name))
+                .Description("The localized name of the property group.");
+
+            Field<StringGraphType>("description")
+                .Resolve(context => GetLocalizedValue(context, context.Source.LocalizedDescription))
+                .Description("The localized description of the property group.");
+        }
+
+        private static string GetLocalizedValue(IResolveFieldContext context, LocalizedString localizedString, string fallbackValue = null)
+        {
+            var cultureName = context.GetArgumentOrValue<string>("cultureName");
+
+            if (!string.IsNullOrEmpty(cultureName))
+            {
+                var localizedValue = localizedString?.GetValue(cultureName);
+                if (!string.IsNullOrEmpty(localizedValue))
+                {
+                    return localizedValue;
+                }
+            }
+
+            return fallbackValue;
+        }
+    }
+}
diff --git a/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs b/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs
index e078cb7..d7fff11 100644
--- a/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs
+++ b/src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs
@@ -1,13 +1,16 @@
 using System.Linq;
 using System.Threading.Tasks;
+using GraphQL;
 using GraphQL.Builders;
 using GraphQL.DataLoader;
+using GraphQL.Resolvers;
 using GraphQL.Types;
 using MediatR;
 using VirtoCommerce.CatalogModule.Core.Model;
 using VirtoCommerce.CatalogModule.Core.Services;
 using VirtoCommerce.Platform.Core.Common;
 using VirtoCommerce.Xapi.Core.Extensions;
+using VirtoCommerce.Xapi.Core.Helpers;
 using VirtoCommerce.Xapi.Core.Infrastructure;
 using VirtoCommerce.Xapi.Core.Schemas;
 using VirtoCommerce.XCatalog.Core.Extensions;
@@ -21,7 +24,7 @@ public class PropertyType : ExtendableGraphType<Property>
     {
         private readonly IMeasureService _measureService;
 
-        public PropertyType(IMediator mediator, IDataLoaderContextAccessor dataLoader, IMeasureService measureService)
+        public PropertyType(IMediator mediator, IDataLoaderContextAccessor dataLoader, IMeasureService measureService, IPropertyGroupService propertyGroupService)
         {
             _measureService = measureService;
 
@@ -73,6 +76,22 @@ public PropertyType(IMediator mediator, IDataLoaderContextAccessor dataLoader, I
             Field<StringGraphType>("valueId")
                 .Resolve(context => context.Source.Values.Select(x => x.ValueId).FirstOrDefault());
 
+            var group = new FieldType
+            {
+                Name = "group",
+                Type = GraphTypeExtensionHelper.GetActualType<PropertyGroupType>(),
+                Resolver = new FuncFieldResolver<Property, IDataLoaderResult<PropertyGroup>>(context =>
+                {
+                    var loader = dataLoader.Context.GetOrAddBatchLoader<string, PropertyGroup>("propertyGroupLoader", async (ids) =>
+                    {
+                        var groups = await propertyGroupService.GetNoCloneAsync(ids.ToList());
+                        return groups.ToDictionary(x => x.Id);
+                    });
+                    return loader.LoadAsync(context.Source.PropertyGroupId);
+                })
+            };
+            AddField(group);
+
             Connection<PropertyDictionaryItemType>("propertyDictionaryItems")
                 .PageSize(Connections.DefaultPageSize)
                 .ResolveAsync(async context =>
diff --git a/src/VirtoCommerce.XCatalog.Core/VirtoCommerce.XCatalog.Core.csproj b/src/VirtoCommerce.XCatalog.Core/VirtoCommerce.XCatalog.Core.csproj
index d12d8ea..47b27cb 100644
--- a/src/VirtoCommerce.XCatalog.Core/VirtoCommerce.XCatalog.Core.csproj
+++ b/src/VirtoCommerce.XCatalog.Core/VirtoCommerce.XCatalog.Core.csproj
@@ -9,7 +9,7 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
-    <PackageReference Include="VirtoCommerce.CatalogModule.Core" Version="3.855.0" />
+    <PackageReference Include="VirtoCommerce.CatalogModule.Core" Version="3.858.0" />
     <PackageReference Include="VirtoCommerce.InventoryModule.Core" Version="3.805.0" />
     <PackageReference Include="VirtoCommerce.MarketingModule.Core" Version="3.815.0" />
     <PackageReference Include="VirtoCommerce.PricingModule.Core" Version="3.809.0" />
diff --git a/src/VirtoCommerce.XCatalog.Web/module.manifest b/src/VirtoCommerce.XCatalog.Web/module.manifest
index a5863f8..96144ee 100644
--- a/src/VirtoCommerce.XCatalog.Web/module.manifest
+++ b/src/VirtoCommerce.XCatalog.Web/module.manifest
@@ -6,7 +6,7 @@
 
   <platformVersion>3.877.0</platformVersion>
   <dependencies>
-    <dependency id="VirtoCommerce.Catalog" version="3.855.0" />
+    <dependency id="VirtoCommerce.Catalog" version="3.858.0" />
     <dependency id="VirtoCommerce.Inventory" version="3.805.0" optional="true" />
     <dependency id="VirtoCommerce.Marketing" version="3.815.0" optional="true" />
     <dependency id="VirtoCommerce.Pricing" version="3.809.0" optional="true" />
diff --git a/tests/VirtoCommerce.XCatalog.Tests/Schemas/PropertyTypeTests.cs b/tests/VirtoCommerce.XCatalog.Tests/Schemas/PropertyTypeTests.cs
index b4216f0..77322fc 100644
--- a/tests/VirtoCommerce.XCatalog.Tests/Schemas/PropertyTypeTests.cs
+++ b/tests/VirtoCommerce.XCatalog.Tests/Schemas/PropertyTypeTests.cs
@@ -14,7 +14,7 @@ namespace VirtoCommerce.XCatalog.Tests.Schemas
 {
     public class PropertyTypeTests : XCatalogMoqHelper
     {
-        private readonly PropertyType _propertyType = new(null, null, null);
+        private readonly PropertyType _propertyType = new(null, null, null, null);
 
         [Fact]
         public async Task PropertyType_Properties_ShouldFilterPropertiesByCultureName()


Reviews:
user: login: OlegoO
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T10:15:25Z
user: login: artem-dudarev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:28:32Z
user: login: ksavosteev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T07:14:43Z
user: login: artem-dudarev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T10:59:36Z
user: login: vc-ci
body: Test Suite: Test Suites/Modules/module_Assets
Tests: 13
Failures: 0
Errors: 0
Time: 7.511
Timestamp: 28-04-2025T14:49:43
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T14:56:34Z

Review comments:
diff_hunk: @@ -0,0 +1,44 @@
+using GraphQL.Types;
+using VirtoCommerce.CatalogModule.Core.Model;
+using VirtoCommerce.Xapi.Core.Extensions;
+using VirtoCommerce.Xapi.Core.Schemas;
+
+namespace VirtoCommerce.XCatalog.Core.Schemas
+{
+    public class PropertyGroupType : ExtendableGraphType<PropertyGroup>
+    {
+        public PropertyGroupType()
+        {
+            Name = "PropertyGroup";
+            Description = "Property group.";
+
+            Field(x => x.Id, nullable: false).Description("The unique ID of the property group.");
+            Field(x => x.Name, nullable: false).Description("The name of the property group.");
+            Field(x => x.Priority, nullable: true).Description("The display order of the property group.");
+
+            Field<StringGraphType>("localizedName").Resolve(context =>
path: src/VirtoCommerce.XCatalog.Core/Schemas/PropertyGroupType.cs
user: login: OlegoO
body: Should be only name + description (that resolve value from name + localizedName and localizedDescription)
created_at: 2025-04-11T10:14:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,6 +71,22 @@ public PropertyType(IMediator mediator, IDataLoaderContextAccessor dataLoader)
             Field<StringGraphType>("valueId")
                 .Resolve(context => context.Source.Values.Select(x => x.ValueId).FirstOrDefault());
 
+            var group = new FieldType
+            {
+                Name = "group",
+                Type = GraphTypeExtensionHelper.GetActualType<PropertyGroupType>(),
+                Resolver = new FuncFieldResolver<Property, IDataLoaderResult<PropertyGroup>>(context =>
+                {
+                    var loader = dataLoader.Context.GetOrAddBatchLoader<string, PropertyGroup>("propertyGroupLoader", async (ids) =>
+                    {
+                        var groups = await propertyGroupService.GetNoCloneAsync(ids.ToList());
+                        return groups.ToDictionary(x => x.Id);
+                    });
+                    return loader.LoadAsync(context.Source.PropertyGroupId);
+                })
+            };
+            AddField(group);
path: src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs
user: login: artem-dudarev
body: There will be a lot of duplicate data in the response if many properties belong to the same property group
created_at: 2025-04-17T09:28:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,6 +71,22 @@ public PropertyType(IMediator mediator, IDataLoaderContextAccessor dataLoader)
             Field<StringGraphType>("valueId")
                 .Resolve(context => context.Source.Values.Select(x => x.ValueId).FirstOrDefault());
 
+            var group = new FieldType
+            {
+                Name = "group",
+                Type = GraphTypeExtensionHelper.GetActualType<PropertyGroupType>(),
+                Resolver = new FuncFieldResolver<Property, IDataLoaderResult<PropertyGroup>>(context =>
+                {
+                    var loader = dataLoader.Context.GetOrAddBatchLoader<string, PropertyGroup>("propertyGroupLoader", async (ids) =>
+                    {
+                        var groups = await propertyGroupService.GetNoCloneAsync(ids.ToList());
+                        return groups.ToDictionary(x => x.Id);
+                    });
+                    return loader.LoadAsync(context.Source.PropertyGroupId);
+                })
+            };
+            AddField(group);
path: src/VirtoCommerce.XCatalog.Core/Schemas/PropertyType.cs
user: login: ksavosteev
body: @artem-dudarev 
There won't be duplication even if all properties have the same property group - Dataloader takes care of data duplication problem

created_at: 2025-04-21T07:14:43Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-28T14:43:29Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [23.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-x-catalog&pullRequest=44)


author_association: NONE