diff --git a/Buildenator/CodeAnalysis/ITypedSymbol.cs b/Buildenator/CodeAnalysis/ITypedSymbol.cs index a693a13..731aea1 100644 --- a/Buildenator/CodeAnalysis/ITypedSymbol.cs +++ b/Buildenator/CodeAnalysis/ITypedSymbol.cs @@ -7,6 +7,12 @@ internal interface ITypedSymbol string SymbolName { get; } string SymbolPascalName { get; } string TypeFullName { get; } + /// + /// Gets the type name suitable for use as a NullBox generic parameter. + /// For nullable reference types, returns the non-nullable version. + /// For nullable value types, returns the type as-is since Nullable<T> is the actual type. + /// + string NonNullableTypeFullName { get; } string TypeName { get; } string UnderScoreName { get; } @@ -110,4 +116,15 @@ public InterfaceDictionaryMetadata(ITypeSymbol keyType, ITypeSymbol valueType, I KeyTypeDisplayName = keyType.ToDisplayString(); ValueTypeDisplayName = valueType.ToDisplayString(); } +} + +/// +/// Metadata for array types (e.g., T[]). +/// +internal sealed class ArrayCollectionMetadata : CollectionMetadata +{ + public ArrayCollectionMetadata(ITypeSymbol elementType) + : base(elementType) + { + } } \ No newline at end of file diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs index 68d6e67..bb620de 100644 --- a/Buildenator/CodeAnalysis/TypedSymbol.cs +++ b/Buildenator/CodeAnalysis/TypedSymbol.cs @@ -56,6 +56,35 @@ private TypedSymbol( private string? _typeFullName; public string TypeFullName => _typeFullName ??= Type.ToDisplayString(); + private string? _nonNullableTypeFullName; + /// + /// Gets the type name suitable for use as a NullBox generic parameter. + /// For nullable reference types (e.g., "string?", "Dictionary<K,V>?"), returns the non-nullable version. + /// For nullable value types (e.g., "int?"), returns the type as-is since Nullable<T> is the actual type. + /// + public string NonNullableTypeFullName => _nonNullableTypeFullName ??= GetNonNullableTypeFullName(); + + private string GetNonNullableTypeFullName() + { + // For nullable value types (e.g., int?, DateTime?), keep the nullable form + // because Nullable is the actual type, not just an annotation + if (Type.TypeKind == TypeKind.Struct && Type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return TypeFullName; + } + + // For nullable reference types, strip the nullable annotation + // This handles cases like string?, Dictionary?, etc. + if (Type.NullableAnnotation == NullableAnnotation.Annotated) + { + return Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString(); + } + + // For non-nullable types, return as-is + return TypeFullName; + } + public string TypeName => Type.Name; public string SymbolPascalName => Symbol.PascalCaseName(); diff --git a/Buildenator/Configuration/CollectionMethodDetector.cs b/Buildenator/Configuration/CollectionMethodDetector.cs index f23fbe3..1a97423 100644 --- a/Buildenator/Configuration/CollectionMethodDetector.cs +++ b/Buildenator/Configuration/CollectionMethodDetector.cs @@ -9,6 +9,7 @@ internal static class CollectionMethodDetector /// /// Factory method that creates CollectionMetadata for a given type symbol. /// Returns ConcreteDictionaryMetadata or InterfaceDictionaryMetadata for dictionary types, + /// ArrayCollectionMetadata for array types, /// ConcreteCollectionMetadata for concrete collection types, /// InterfaceCollectionMetadata for interface collection types, /// or null if the type is not a collection. @@ -23,6 +24,12 @@ internal static class CollectionMethodDetector return dictionaryMetadata; } + // Check for array types + if (propertyType is IArrayTypeSymbol arrayType) + { + return new ArrayCollectionMetadata(arrayType.ElementType); + } + // Check for concrete collection types (before interface check) if (IsConcreteCollectionProperty(propertyType)) { diff --git a/Buildenator/Generators/ConstructorsGenerator.cs b/Buildenator/Generators/ConstructorsGenerator.cs index 13bc7cf..19c7dc5 100644 --- a/Buildenator/Generators/ConstructorsGenerator.cs +++ b/Buildenator/Generators/ConstructorsGenerator.cs @@ -84,11 +84,12 @@ private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSy { var fieldName = typedSymbol.UnderScoreName; var typeFullName = typedSymbol.TypeFullName; + var nonNullableTypeFullName = typedSymbol.NonNullableTypeFullName; // For concrete dictionary types, create new instance if (collectionMetadata is ConcreteDictionaryMetadata) { - return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {typeFullName}());"; + return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {nonNullableTypeFullName}());"; } // For interface dictionary types, create a Dictionary @@ -103,7 +104,7 @@ private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSy // Standard .NET collections (List, HashSet, Collection, etc.) all support this. if (collectionMetadata is ConcreteCollectionMetadata) { - return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {typeFullName}());"; + return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {nonNullableTypeFullName}());"; } // For interface collection types, create a List diff --git a/Buildenator/Generators/PropertiesStringGenerator.cs b/Buildenator/Generators/PropertiesStringGenerator.cs index b330061..083177c 100644 --- a/Buildenator/Generators/PropertiesStringGenerator.cs +++ b/Buildenator/Generators/PropertiesStringGenerator.cs @@ -189,9 +189,12 @@ private string GenerateMethodDefinitionHeader(ITypedSymbol typedSymbol) => $"public {_builder.FullName} {CreateMethodName(typedSymbol)}({typedSymbol.GenerateMethodParameterDefinition()})"; private static string GenerateValueAssignment(ITypedSymbol typedSymbol) - => typedSymbol.IsMockable() - ? $"{DefaultConstants.SetupActionLiteral}({typedSymbol.UnderScoreName})" - : $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({DefaultConstants.ValueLiteral})"; + { + if (typedSymbol.IsMockable()) + return $"{DefaultConstants.SetupActionLiteral}({typedSymbol.UnderScoreName})"; + + return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({DefaultConstants.ValueLiteral})"; + } private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}"; @@ -219,7 +222,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) }} else {{ - dictionary = new {typedSymbol.TypeFullName}(); + dictionary = new {typedSymbol.NonNullableTypeFullName}(); }} foreach (var item in items) @@ -252,27 +255,45 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) }}"; } - // For concrete types, use new() and .Add() method from ICollection - if (collectionMetadata is ConcreteCollectionMetadata) + // For array types and concrete collection types, use similar pattern + if (collectionMetadata is ArrayCollectionMetadata or ConcreteCollectionMetadata) { - return $@"public {_builder.FullName} {methodName}(params {elementTypeName}[] items) - {{ - {typedSymbol.TypeFullName} collection; - if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + var isArray = collectionMetadata is ArrayCollectionMetadata; + var collectionVarName = isArray ? "array" : "collection"; + var collectionTypeName = isArray ? $"{elementTypeName}[]" : typedSymbol.TypeFullName; + + var addItemsCode = isArray + ? $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) {{ - collection = {fieldName}.Value.Object; + var existingArray = {fieldName}.Value.Object; + {collectionVarName} = new {elementTypeName}[existingArray.Length + items.Length]; + System.Array.Copy(existingArray, 0, {collectionVarName}, 0, existingArray.Length); + System.Array.Copy(items, 0, {collectionVarName}, existingArray.Length, items.Length); }} else {{ - collection = new {typedSymbol.TypeFullName}(); + {collectionVarName} = items; + }}" + : $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + {{ + {collectionVarName} = {fieldName}.Value.Object; + }} + else + {{ + {collectionVarName} = new {typedSymbol.NonNullableTypeFullName}(); }} foreach (var item in items) {{ - collection.Add(item); - }} + {collectionVarName}.Add(item); + }}"; + + return $@"public {_builder.FullName} {methodName}(params {elementTypeName}[] items) + {{ + {collectionTypeName} {collectionVarName}; + {addItemsCode} - {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection); + {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({collectionVarName}); return this; }}"; } @@ -315,13 +336,12 @@ private string GenerateChildBuilderMethodDefinition(ITypedSymbol typedSymbol) var methodName = CreateMethodName(typedSymbol); var fieldName = typedSymbol.UnderScoreName; - var entityTypeName = typedSymbol.TypeFullName; return $@"public {_builder.FullName} {methodName}(System.Func<{childBuilderName}, {childBuilderName}> configure{typedSymbol.SymbolPascalName}) {{ var childBuilder = new {childBuilderName}(); childBuilder = configure{typedSymbol.SymbolPascalName}(childBuilder); - {fieldName} = new {DefaultConstants.NullBox}<{entityTypeName}>(childBuilder.Build()); + {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(childBuilder.Build()); return this; }}"; } @@ -335,29 +355,66 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo var methodName = CreateAddToMethodName(typedSymbol); var fieldName = typedSymbol.UnderScoreName; - // For concrete collection types - if (collectionMetadata is ConcreteCollectionMetadata) + // For array types and concrete collection types, use similar pattern + if (collectionMetadata is ArrayCollectionMetadata or ConcreteCollectionMetadata) { - return $@"public {_builder.FullName} {methodName}(params System.Func<{childBuilderName}, {childBuilderName}>[] configures) - {{ - {typedSymbol.TypeFullName} collection; - if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + var isArray = collectionMetadata is ArrayCollectionMetadata; + var collectionVarName = isArray ? "array" : "collection"; + var collectionTypeName = isArray ? $"{elementTypeName}[]" : typedSymbol.TypeFullName; + + // Build child items first - for arrays we need to know the count upfront + var buildItemsCode = isArray + ? $@"var newItems = new {elementTypeName}[configures.Length]; + for (int i = 0; i < configures.Length; i++) + {{ + var childBuilder = new {childBuilderName}(); + childBuilder = configures[i](childBuilder); + newItems[i] = childBuilder.Build(); + }}" + : ""; + + var addItemsCode = isArray + ? $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + {{ + var existingArray = {fieldName}.Value.Object; + {collectionVarName} = new {elementTypeName}[existingArray.Length + newItems.Length]; + System.Array.Copy(existingArray, 0, {collectionVarName}, 0, existingArray.Length); + System.Array.Copy(newItems, 0, {collectionVarName}, existingArray.Length, newItems.Length); + }} + else + {{ + {collectionVarName} = newItems; + }}" + : $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) {{ - collection = {fieldName}.Value.Object; + {collectionVarName} = {fieldName}.Value.Object; }} else {{ - collection = new {typedSymbol.TypeFullName}(); + {collectionVarName} = new {typedSymbol.NonNullableTypeFullName}(); }} foreach (var configure in configures) {{ var childBuilder = new {childBuilderName}(); childBuilder = configure(childBuilder); - collection.Add(childBuilder.Build()); - }} + {collectionVarName}.Add(childBuilder.Build()); + }}"; + + // For arrays, we need extra newlines between buildItemsCode and the variable declaration + var methodBody = isArray + ? $@"{buildItemsCode} + + {collectionTypeName} {collectionVarName}; + {addItemsCode}" + : $@"{collectionTypeName} {collectionVarName}; + {addItemsCode}"; + + return $@"public {_builder.FullName} {methodName}(params System.Func<{childBuilderName}, {childBuilderName}>[] configures) + {{ + {methodBody} - {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection); + {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({collectionVarName}); return this; }}"; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4876234..e825538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 8.7.1.0 - 2025-12-4 + +### Fixed +- **Array collection support**: Fixed an issue where `AddTo...` methods were not generated for array properties when `useChildBuilders` is enabled. Arrays now receive the same treatment as other collection types: + - `AddTo` methods with `params T[]` parameters for regular value additions + - `AddTo` methods with `params Func[]` parameters when `useChildBuilders: true` is set + - Arrays are properly concatenated when using `AddTo` multiple times + - Both constructor parameter arrays and settable property arrays are supported +- **Nullable collections support**: Fixed compilation error when models have nullable collection properties (e.g., `Dictionary?`, `IDictionary?`, `IReadOnlyDictionary?`). The generator now correctly handles nullable reference types in `NullBox` type parameters by stripping the nullable annotation. + ## 8.7.0.0 - 2025-11-27 ### Added diff --git a/Tests/Buildenator.IntegrationTests.SharedEntities/ParentWithChildArrayEntity.cs b/Tests/Buildenator.IntegrationTests.SharedEntities/ParentWithChildArrayEntity.cs new file mode 100644 index 0000000..80b05a8 --- /dev/null +++ b/Tests/Buildenator.IntegrationTests.SharedEntities/ParentWithChildArrayEntity.cs @@ -0,0 +1,21 @@ +namespace Buildenator.IntegrationTests.SharedEntities; + +/// +/// A parent entity that contains arrays of child entities, used to test UseChildBuilders feature with arrays. +/// +public class ParentWithChildArrayEntity +{ + public ParentWithChildArrayEntity(ChildForParentEntity[] children, int parentValue) + { + Children = children; + ParentValue = parentValue; + } + + public ChildForParentEntity[] Children { get; } + public int ParentValue { get; } + + /// + /// A settable property with an array of children for testing AddTo methods with child builder configuration. + /// + public ChildForParentEntity[] OptionalChildren { get; set; } = []; +} diff --git a/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs new file mode 100644 index 0000000..68da425 --- /dev/null +++ b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Buildenator.IntegrationTests.SharedEntitiesNullable +{ + /// + /// Entity used to test nullable dictionary handling in generated builders. + /// Tests various nullable dictionary types. + /// + public class EntityWithNullableDictionary + { + /// + /// Nullable Dictionary property + /// + public Dictionary? Metadata { get; set; } + + /// + /// Nullable IDictionary property + /// + public IDictionary? Items { get; set; } + + /// + /// Nullable IReadOnlyDictionary property + /// + public IReadOnlyDictionary? Settings { get; set; } + + /// + /// Another nullable Dictionary property + /// + public Dictionary? Scores { get; set; } + + /// + /// Simple non-dictionary property for comparison + /// + public string? Name { get; set; } + + /// + /// Nullable value type property to test int? handling + /// + public int? Count { get; set; } + } +} diff --git a/Tests/Buildenator.IntegrationTests.Source/Builders/ParentWithChildArrayEntityBuilder.cs b/Tests/Buildenator.IntegrationTests.Source/Builders/ParentWithChildArrayEntityBuilder.cs new file mode 100644 index 0000000..1d43232 --- /dev/null +++ b/Tests/Buildenator.IntegrationTests.Source/Builders/ParentWithChildArrayEntityBuilder.cs @@ -0,0 +1,13 @@ +using Buildenator.Abstraction; +using Buildenator.IntegrationTests.SharedEntities; + +namespace Buildenator.IntegrationTests.Source.Builders; + +/// +/// Builder for ParentWithChildArrayEntity with UseChildBuilders enabled. +/// This will generate AddTo methods that accept Func<ChildBuilder, ChildBuilder> for arrays containing buildable entities. +/// +[MakeBuilder(typeof(ParentWithChildArrayEntity), generateDefaultBuildMethod: false, useChildBuilders: true)] +public partial class ParentWithChildArrayEntityBuilder +{ +} diff --git a/Tests/Buildenator.IntegrationTests.SourceNullable/Builders/EntityWithNullableDictionaryBuilder.cs b/Tests/Buildenator.IntegrationTests.SourceNullable/Builders/EntityWithNullableDictionaryBuilder.cs new file mode 100644 index 0000000..e4f4b6f --- /dev/null +++ b/Tests/Buildenator.IntegrationTests.SourceNullable/Builders/EntityWithNullableDictionaryBuilder.cs @@ -0,0 +1,10 @@ +using Buildenator.Abstraction; +using Buildenator.IntegrationTests.SharedEntitiesNullable; + +namespace Buildenator.IntegrationTests.SourceNullable.Builders +{ + [MakeBuilder(typeof(EntityWithNullableDictionary))] + public partial class EntityWithNullableDictionaryBuilder + { + } +} diff --git a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs index f8e44cc..738eb6d 100644 --- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs +++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs @@ -258,5 +258,110 @@ public void BuildersGenerator_ReadOnlyProperty_ShouldCreateMethodForSettingItsVa builder.WithPrivateField(privateField); builder.Build().PrivateField.Should().BeEquivalentTo(privateField); } + + [Theory] + [AutoData] + public void BuildersGenerator_NullableDictionary_WithMethodShouldSetValue(string key, string value) + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + var metadata = new Dictionary { { key, value } }; + + // Act + var result = builder + .WithMetadata(metadata) + .Build(); + + // Assert + _ = result.Metadata.Should().NotBeNull(); + _ = result.Metadata.Should().HaveCount(1); + _ = result.Metadata![key].Should().Be(value); + } + + [Theory] + [AutoData] + public void BuildersGenerator_NullableIDictionary_WithMethodShouldSetValue(int key, string value) + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + var items = new Dictionary { { key, value } }; + + // Act + var result = builder + .WithItems(items) + .Build(); + + // Assert + _ = result.Items.Should().NotBeNull(); + _ = result.Items.Should().HaveCount(1); + _ = result.Items![key].Should().Be(value); + } + + [Fact] + public void BuildersGenerator_NullableDictionary_WithNullValueShouldWork() + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + + // Act + var result = builder + .WithMetadata(null) + .Build(); + + // Assert + _ = result.Metadata.Should().BeNull(); + } + + [Theory] + [AutoData] + public void BuildersGenerator_NullableDictionary_AddToMethodShouldAddItems(string key1, string value1, string key2, string value2) + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + + // Act + var result = builder + .AddToMetadata( + new KeyValuePair(key1, value1), + new KeyValuePair(key2, value2)) + .Build(); + + // Assert + _ = result.Metadata.Should().NotBeNull(); + _ = result.Metadata.Should().HaveCount(2); + _ = result.Metadata![key1].Should().Be(value1); + _ = result.Metadata[key2].Should().Be(value2); + } + + [Theory] + [AutoData] + public void BuildersGenerator_NullableValueType_WithMethodShouldSetValue(int value) + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + + // Act + var result = builder + .WithCount(value) + .Build(); + + // Assert + _ = result.Count.Should().Be(value); + } + + [Fact] + public void BuildersGenerator_NullableValueType_WithNullValueShouldWork() + { + // Arrange + var builder = EntityWithNullableDictionaryBuilder.EntityWithNullableDictionary; + + // Act + var result = builder + .WithCount(null) + .Build(); + + // Assert + _ = result.Count.Should().BeNull(); + } } } diff --git a/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs index 5155e60..ffab40d 100644 --- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs +++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs @@ -1459,6 +1459,108 @@ public void BuildersGenerator_UseChildBuildersWithCollection_TraditionalMethodsS _ = result.OptionalChildren[0].Name.Should().Be(childName); } + // ===== Tests for UseChildBuilders with Arrays ===== + // These tests verify that child builder methods are generated for arrays when useChildBuilders is enabled + + [Theory] + [AutoData] + public void BuildersGenerator_UseChildBuildersWithArray_AddToMethodShouldAcceptFuncParameter( + string childName1, int childValue1, string childName2, int childValue2, int parentValue) + { + // Arrange - ParentWithChildArrayEntityBuilder has useChildBuilders: true + var builder = ParentWithChildArrayEntityBuilder.ParentWithChildArrayEntity; + + // Act - Use the generated AddTo method with Func parameters + var result = builder + .AddToChildren( + child => child.WithName(childName1).WithValue(childValue1), + child => child.WithName(childName2).WithValue(childValue2)) + .WithParentValue(parentValue) + .Build(); + + // Assert + _ = result.Should().NotBeNull(); + _ = result.Children.Should().HaveCount(2); + _ = result.Children[0].Name.Should().Be(childName1); + _ = result.Children[0].Value.Should().Be(childValue1); + _ = result.Children[1].Name.Should().Be(childName2); + _ = result.Children[1].Value.Should().Be(childValue2); + _ = result.ParentValue.Should().Be(parentValue); + } + + [Theory] + [AutoData] + public void BuildersGenerator_UseChildBuildersWithArray_SettablePropertyAddToShouldAcceptFuncParameter( + string childName1, int childValue1, string childName2, int childValue2, int parentValue) + { + // Arrange + var builder = ParentWithChildArrayEntityBuilder.ParentWithChildArrayEntity; + + // Act - Use the AddTo method for the settable OptionalChildren array property + var result = builder + .AddToChildren(child => child.WithName("constructor-child").WithValue(0)) + .WithParentValue(parentValue) + .AddToOptionalChildren( + child => child.WithName(childName1).WithValue(childValue1), + child => child.WithName(childName2).WithValue(childValue2)) + .Build(); + + // Assert + _ = result.Should().NotBeNull(); + _ = result.OptionalChildren.Should().HaveCount(2); + _ = result.OptionalChildren[0].Name.Should().Be(childName1); + _ = result.OptionalChildren[0].Value.Should().Be(childValue1); + _ = result.OptionalChildren[1].Name.Should().Be(childName2); + _ = result.OptionalChildren[1].Value.Should().Be(childValue2); + } + + [Theory] + [AutoData] + public void BuildersGenerator_UseChildBuildersWithArray_MultipleAddToCalls_ShouldAccumulate( + string childName1, int childValue1, string childName2, int childValue2, int parentValue) + { + // Arrange + var builder = ParentWithChildArrayEntityBuilder.ParentWithChildArrayEntity; + + // Act - Use AddTo multiple times - items should accumulate in the array + var result = builder + .AddToChildren(child => child.WithName(childName1).WithValue(childValue1)) + .AddToChildren(child => child.WithName(childName2).WithValue(childValue2)) + .WithParentValue(parentValue) + .Build(); + + // Assert + _ = result.Should().NotBeNull(); + _ = result.Children.Should().HaveCount(2); + _ = result.Children[0].Name.Should().Be(childName1); + _ = result.Children[1].Name.Should().Be(childName2); + } + + [Theory] + [AutoData] + public void BuildersGenerator_UseChildBuildersWithArray_TraditionalMethodsStillWork( + string childName, int childValue, int parentValue) + { + // Arrange - This test verifies that both traditional With/AddTo methods and Func-based methods are generated + // Compilation success is the test - if the methods don't exist, this won't compile + var child = new ChildForParentEntity(childName, childValue); + var builder = ParentWithChildArrayEntityBuilder.ParentWithChildArrayEntity; + + // Act - Use the traditional WithChildren and AddToChildren methods + var result = builder + .WithChildren(new[] { child }) // Traditional With method + .AddToOptionalChildren(child) // Traditional AddTo method for settable property + .WithParentValue(parentValue) + .Build(); + + // Assert + _ = result.Should().NotBeNull(); + _ = result.Children.Should().HaveCount(1); + _ = result.Children[0].Name.Should().Be(childName); + _ = result.OptionalChildren.Should().HaveCount(1); + _ = result.OptionalChildren[0].Name.Should().Be(childName); + } + // ===== Tests for UseChildBuilders: false ===== // These tests verify that Func<> methods are NOT generated when useChildBuilders is disabled