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