From 62a17f878662fc654938eae4a81940dc3593f811 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 10:24:58 +0000
Subject: [PATCH 1/8] Initial plan
From a3cdf9a49e6030fa70c3fca0bbd87ed6f08e87cc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 10:30:36 +0000
Subject: [PATCH 2/8] Add test case for nullable dictionary support
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
.../EntityWithNullableDictionary.cs | 36 +++++++++++++++++++
.../EntityWithNullableDictionaryBuilder.cs | 10 ++++++
2 files changed, 46 insertions(+)
create mode 100644 Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
create mode 100644 Tests/Buildenator.IntegrationTests.SourceNullable/Builders/EntityWithNullableDictionaryBuilder.cs
diff --git a/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
new file mode 100644
index 0000000..a36a9be
--- /dev/null
+++ b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
@@ -0,0 +1,36 @@
+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; }
+ }
+}
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
+ {
+ }
+}
From 3bed741704c0512d01fccaf089a8cad68a0fd506 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 10:38:27 +0000
Subject: [PATCH 3/8] Fix nullable dictionary support in builder generation
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
Buildenator/CodeAnalysis/ITypedSymbol.cs | 1 +
Buildenator/CodeAnalysis/TypedSymbol.cs | 7 +-
.../Generators/ConstructorsGenerator.cs | 2 +-
.../Generators/PropertiesStringGenerator.cs | 34 +++++----
.../BuildersGeneratorNullableTests.cs | 74 +++++++++++++++++++
5 files changed, 101 insertions(+), 17 deletions(-)
diff --git a/Buildenator/CodeAnalysis/ITypedSymbol.cs b/Buildenator/CodeAnalysis/ITypedSymbol.cs
index a693a13..be3c777 100644
--- a/Buildenator/CodeAnalysis/ITypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/ITypedSymbol.cs
@@ -7,6 +7,7 @@ internal interface ITypedSymbol
string SymbolName { get; }
string SymbolPascalName { get; }
string TypeFullName { get; }
+ string NonNullableTypeFullName { get; }
string TypeName { get; }
string UnderScoreName { get; }
diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs
index 68d6e67..943d616 100644
--- a/Buildenator/CodeAnalysis/TypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/TypedSymbol.cs
@@ -56,6 +56,9 @@ private TypedSymbol(
private string? _typeFullName;
public string TypeFullName => _typeFullName ??= Type.ToDisplayString();
+ private string? _nonNullableTypeFullName;
+ public string NonNullableTypeFullName => _nonNullableTypeFullName ??= Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
+
public string TypeName => Type.Name;
public string SymbolPascalName => Symbol.PascalCaseName();
@@ -135,12 +138,12 @@ public string GenerateFieldType()
}
public string GenerateLazyFieldType()
- => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{TypeFullName}>?";
+ => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{NonNullableTypeFullName}>?";
public string GenerateLazyFieldValueReturn()
=> IsMockable()
? string.Format(_mockingProperties!.ReturnObjectFormat, UnderScoreName)
- : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{TypeFullName}>({(IsFakeable()
+ : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{NonNullableTypeFullName}>({(IsFakeable()
? $"{string.Format(_fixtureProperties!.CreateSingleFormat, TypeFullName, SymbolName, DefaultConstants.FixtureLiteral)}"
+ (_nullableStrategy == NullableStrategy.Enabled ? "!" : "")
: $"default({TypeFullName})")})).Object";
diff --git a/Buildenator/Generators/ConstructorsGenerator.cs b/Buildenator/Generators/ConstructorsGenerator.cs
index 13bc7cf..13d7345 100644
--- a/Buildenator/Generators/ConstructorsGenerator.cs
+++ b/Buildenator/Generators/ConstructorsGenerator.cs
@@ -83,7 +83,7 @@ private static bool ShouldInitializeCollectionField(ITypedSymbol typedSymbol)
private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSymbol, CollectionMetadata collectionMetadata)
{
var fieldName = typedSymbol.UnderScoreName;
- var typeFullName = typedSymbol.TypeFullName;
+ var typeFullName = typedSymbol.NonNullableTypeFullName;
// For concrete dictionary types, create new instance
if (collectionMetadata is ConcreteDictionaryMetadata)
diff --git a/Buildenator/Generators/PropertiesStringGenerator.cs b/Buildenator/Generators/PropertiesStringGenerator.cs
index b330061..e18d839 100644
--- a/Buildenator/Generators/PropertiesStringGenerator.cs
+++ b/Buildenator/Generators/PropertiesStringGenerator.cs
@@ -175,7 +175,7 @@ private string GenerateFieldInitializer(ITypedSymbol typedSymbol)
if (defaultValueName is null)
return string.Empty;
- return $" = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({defaultValueName})";
+ return $" = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({defaultValueName})";
}
private string GenerateMethodDefinition(ITypedSymbol typedSymbol)
@@ -189,9 +189,15 @@ 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})";
+
+ // Add null-forgiving operator (!) for nullable reference types to suppress CS8604 warnings
+ // when assigning potentially null values to NullBox where T is non-nullable
+ var nullForgiving = typedSymbol.TypeFullName != typedSymbol.NonNullableTypeFullName ? "!" : "";
+ return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({DefaultConstants.ValueLiteral}{nullForgiving})";
+ }
private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}";
@@ -219,7 +225,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- dictionary = new {typedSymbol.TypeFullName}();
+ dictionary = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var item in items)
@@ -227,7 +233,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
return this;
}}";
}
@@ -247,7 +253,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
{{
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
return this;
}}";
}
@@ -264,7 +270,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- collection = new {typedSymbol.TypeFullName}();
+ collection = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var item in items)
@@ -272,7 +278,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
collection.Add(item);
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
return this;
}}";
}
@@ -284,7 +290,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object)
: new System.Collections.Generic.List<{elementTypeName}>();
list.AddRange(items);
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(({typedSymbol.TypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
return this;
}}";
}
@@ -315,7 +321,7 @@ private string GenerateChildBuilderMethodDefinition(ITypedSymbol typedSymbol)
var methodName = CreateMethodName(typedSymbol);
var fieldName = typedSymbol.UnderScoreName;
- var entityTypeName = typedSymbol.TypeFullName;
+ var entityTypeName = typedSymbol.NonNullableTypeFullName;
return $@"public {_builder.FullName} {methodName}(System.Func<{childBuilderName}, {childBuilderName}> configure{typedSymbol.SymbolPascalName})
{{
@@ -347,7 +353,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
}}
else
{{
- collection = new {typedSymbol.TypeFullName}();
+ collection = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var configure in configures)
@@ -357,7 +363,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
collection.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
return this;
}}";
}
@@ -376,7 +382,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
list.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(({typedSymbol.TypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
return this;
}}";
}
diff --git a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
index f8e44cc..b160562 100644
--- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
+++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
@@ -258,5 +258,79 @@ 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);
+ }
}
}
From 616c4f22c5fd2fc441c4318005a1d1cc755f0df8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 10:41:06 +0000
Subject: [PATCH 4/8] Update CHANGELOG for nullable dictionary fix
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
CHANGELOG.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4876234..f216d04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ 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).
+## Unreleased
+
+### Fixed
+- **Nullable dictionary support**: Fixed compilation error when models have nullable dictionary 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
From a93a181c6ef374f65372b4d4d0aaf3c0457fc565 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 10:46:27 +0000
Subject: [PATCH 5/8] Add XML documentation for NonNullableTypeFullName
property
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
Buildenator/CodeAnalysis/ITypedSymbol.cs | 4 ++++
Buildenator/CodeAnalysis/TypedSymbol.cs | 4 ++++
2 files changed, 8 insertions(+)
diff --git a/Buildenator/CodeAnalysis/ITypedSymbol.cs b/Buildenator/CodeAnalysis/ITypedSymbol.cs
index be3c777..5970470 100644
--- a/Buildenator/CodeAnalysis/ITypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/ITypedSymbol.cs
@@ -7,6 +7,10 @@ internal interface ITypedSymbol
string SymbolName { get; }
string SymbolPascalName { get; }
string TypeFullName { get; }
+ ///
+ /// Gets the full type name without nullable annotation.
+ /// Used for generic type parameters where nullable reference types are not allowed.
+ ///
string NonNullableTypeFullName { get; }
string TypeName { get; }
string UnderScoreName { get; }
diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs
index 943d616..46ef629 100644
--- a/Buildenator/CodeAnalysis/TypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/TypedSymbol.cs
@@ -57,6 +57,10 @@ private TypedSymbol(
public string TypeFullName => _typeFullName ??= Type.ToDisplayString();
private string? _nonNullableTypeFullName;
+ ///
+ /// Gets the full type name without nullable annotation (e.g., "Dictionary<string, string>" instead of "Dictionary<string, string>?").
+ /// This is used for generic type parameters like NullBox<T> where nullable reference types are not allowed.
+ ///
public string NonNullableTypeFullName => _nonNullableTypeFullName ??= Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
public string TypeName => Type.Name;
From 6518f17c17250765bd0336bcc988f74c461feead Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 11:55:35 +0000
Subject: [PATCH 6/8] Refactor to use GetTypeFullNameForGenericParameter
instead of NonNullableTypeFullName
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
Buildenator/CodeAnalysis/ITypedSymbol.cs | 11 +++----
Buildenator/CodeAnalysis/TypedSymbol.cs | 21 ++++++++------
.../Generators/ConstructorsGenerator.cs | 10 +++----
.../Generators/PropertiesStringGenerator.cs | 29 +++++++++----------
4 files changed, 37 insertions(+), 34 deletions(-)
diff --git a/Buildenator/CodeAnalysis/ITypedSymbol.cs b/Buildenator/CodeAnalysis/ITypedSymbol.cs
index 5970470..f7f4940 100644
--- a/Buildenator/CodeAnalysis/ITypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/ITypedSymbol.cs
@@ -7,11 +7,6 @@ internal interface ITypedSymbol
string SymbolName { get; }
string SymbolPascalName { get; }
string TypeFullName { get; }
- ///
- /// Gets the full type name without nullable annotation.
- /// Used for generic type parameters where nullable reference types are not allowed.
- ///
- string NonNullableTypeFullName { get; }
string TypeName { get; }
string UnderScoreName { get; }
@@ -37,6 +32,12 @@ internal interface ITypedSymbol
///
/// The default value name (e.g., "DefaultName") if found, otherwise null.
string? GetDefaultValueName();
+
+ ///
+ /// Returns the type full name suitable for use as a generic type parameter.
+ /// For nullable reference types, strips the nullable annotation to avoid CS8628 errors.
+ ///
+ string GetTypeFullNameForGenericParameter();
}
///
diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs
index 46ef629..ab41818 100644
--- a/Buildenator/CodeAnalysis/TypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/TypedSymbol.cs
@@ -56,13 +56,6 @@ private TypedSymbol(
private string? _typeFullName;
public string TypeFullName => _typeFullName ??= Type.ToDisplayString();
- private string? _nonNullableTypeFullName;
- ///
- /// Gets the full type name without nullable annotation (e.g., "Dictionary<string, string>" instead of "Dictionary<string, string>?").
- /// This is used for generic type parameters like NullBox<T> where nullable reference types are not allowed.
- ///
- public string NonNullableTypeFullName => _nonNullableTypeFullName ??= Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString();
-
public string TypeName => Type.Name;
public string SymbolPascalName => Symbol.PascalCaseName();
@@ -142,12 +135,12 @@ public string GenerateFieldType()
}
public string GenerateLazyFieldType()
- => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{NonNullableTypeFullName}>?";
+ => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{GetTypeFullNameForGenericParameter()}>?";
public string GenerateLazyFieldValueReturn()
=> IsMockable()
? string.Format(_mockingProperties!.ReturnObjectFormat, UnderScoreName)
- : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{NonNullableTypeFullName}>({(IsFakeable()
+ : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{GetTypeFullNameForGenericParameter()}>({(IsFakeable()
? $"{string.Format(_fixtureProperties!.CreateSingleFormat, TypeFullName, SymbolName, DefaultConstants.FixtureLiteral)}"
+ (_nullableStrategy == NullableStrategy.Enabled ? "!" : "")
: $"default({TypeFullName})")})).Object";
@@ -160,6 +153,16 @@ public string GenerateFieldValueReturn()
public string GenerateMethodParameterDefinition()
=> IsMockable() ? $"Action<{GenerateMockableFieldType()}> {DefaultConstants.SetupActionLiteral}" : $"{TypeFullName} {DefaultConstants.ValueLiteral}";
+ ///
+ /// Returns the type full name suitable for use as a generic type parameter.
+ /// For nullable reference types, strips the nullable annotation to avoid CS8628 errors.
+ /// For example, "Dictionary<string, string>?" becomes "Dictionary<string, string>".
+ ///
+ public string GetTypeFullNameForGenericParameter()
+ => Type.NullableAnnotation == NullableAnnotation.Annotated
+ ? Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString()
+ : TypeFullName;
+
public string? GetDefaultValueName()
{
if (_defaultValueNames == null)
diff --git a/Buildenator/Generators/ConstructorsGenerator.cs b/Buildenator/Generators/ConstructorsGenerator.cs
index 13d7345..0d72ca9 100644
--- a/Buildenator/Generators/ConstructorsGenerator.cs
+++ b/Buildenator/Generators/ConstructorsGenerator.cs
@@ -83,19 +83,19 @@ private static bool ShouldInitializeCollectionField(ITypedSymbol typedSymbol)
private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSymbol, CollectionMetadata collectionMetadata)
{
var fieldName = typedSymbol.UnderScoreName;
- var typeFullName = typedSymbol.NonNullableTypeFullName;
+ var typeForGenericParam = typedSymbol.GetTypeFullNameForGenericParameter();
// For concrete dictionary types, create new instance
if (collectionMetadata is ConcreteDictionaryMetadata)
{
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {typeFullName}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {typeForGenericParam}());";
}
// For interface dictionary types, create a Dictionary
if (collectionMetadata is InterfaceDictionaryMetadata dictMetadata)
{
var dictionaryType = $"System.Collections.Generic.Dictionary<{dictMetadata.KeyTypeDisplayName}, {dictMetadata.ValueTypeDisplayName}>";
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {dictionaryType}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {dictionaryType}());";
}
// For concrete collection types, create new instance.
@@ -103,7 +103,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}<{typeForGenericParam}>(new {typeForGenericParam}());";
}
// For interface collection types, create a List
@@ -111,7 +111,7 @@ private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSy
{
var elementTypeName = collectionMetadata.ElementTypeDisplayName;
var listType = $"System.Collections.Generic.List<{elementTypeName}>";
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {listType}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {listType}());";
}
return string.Empty;
diff --git a/Buildenator/Generators/PropertiesStringGenerator.cs b/Buildenator/Generators/PropertiesStringGenerator.cs
index e18d839..54b11e4 100644
--- a/Buildenator/Generators/PropertiesStringGenerator.cs
+++ b/Buildenator/Generators/PropertiesStringGenerator.cs
@@ -175,7 +175,7 @@ private string GenerateFieldInitializer(ITypedSymbol typedSymbol)
if (defaultValueName is null)
return string.Empty;
- return $" = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({defaultValueName})";
+ return $" = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>({defaultValueName})";
}
private string GenerateMethodDefinition(ITypedSymbol typedSymbol)
@@ -193,10 +193,9 @@ private static string GenerateValueAssignment(ITypedSymbol typedSymbol)
if (typedSymbol.IsMockable())
return $"{DefaultConstants.SetupActionLiteral}({typedSymbol.UnderScoreName})";
- // Add null-forgiving operator (!) for nullable reference types to suppress CS8604 warnings
- // when assigning potentially null values to NullBox where T is non-nullable
- var nullForgiving = typedSymbol.TypeFullName != typedSymbol.NonNullableTypeFullName ? "!" : "";
- return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({DefaultConstants.ValueLiteral}{nullForgiving})";
+ // Add null-forgiving operator when the parameter is nullable but NullBox expects non-nullable
+ var nullForgiving = typedSymbol.TypeFullName != typedSymbol.GetTypeFullNameForGenericParameter() ? "!" : "";
+ return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>({DefaultConstants.ValueLiteral}{nullForgiving})";
}
private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}";
@@ -225,7 +224,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- dictionary = new {typedSymbol.NonNullableTypeFullName}();
+ dictionary = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
}}
foreach (var item in items)
@@ -233,7 +232,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(dictionary);
return this;
}}";
}
@@ -253,7 +252,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
{{
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(dictionary);
return this;
}}";
}
@@ -270,7 +269,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- collection = new {typedSymbol.NonNullableTypeFullName}();
+ collection = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
}}
foreach (var item in items)
@@ -278,7 +277,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
collection.Add(item);
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(collection);
return this;
}}";
}
@@ -290,7 +289,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object)
: new System.Collections.Generic.List<{elementTypeName}>();
list.AddRange(items);
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(({typedSymbol.GetTypeFullNameForGenericParameter()})list);
return this;
}}";
}
@@ -321,7 +320,7 @@ private string GenerateChildBuilderMethodDefinition(ITypedSymbol typedSymbol)
var methodName = CreateMethodName(typedSymbol);
var fieldName = typedSymbol.UnderScoreName;
- var entityTypeName = typedSymbol.NonNullableTypeFullName;
+ var entityTypeName = typedSymbol.GetTypeFullNameForGenericParameter();
return $@"public {_builder.FullName} {methodName}(System.Func<{childBuilderName}, {childBuilderName}> configure{typedSymbol.SymbolPascalName})
{{
@@ -353,7 +352,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
}}
else
{{
- collection = new {typedSymbol.NonNullableTypeFullName}();
+ collection = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
}}
foreach (var configure in configures)
@@ -363,7 +362,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
collection.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(collection);
return this;
}}";
}
@@ -382,7 +381,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
list.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(({typedSymbol.GetTypeFullNameForGenericParameter()})list);
return this;
}}";
}
From 8ffbe25aaa7f4235449b4ca8a537492bd54a52be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 12:29:34 +0000
Subject: [PATCH 7/8] Fix NonNullableTypeFullName to preserve nullable value
types (int?, etc.)
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
Buildenator/CodeAnalysis/ITypedSymbol.cs | 12 +++---
Buildenator/CodeAnalysis/TypedSymbol.cs | 43 +++++++++++++------
.../Generators/ConstructorsGenerator.cs | 10 ++---
.../Generators/PropertiesStringGenerator.cs | 29 +++++++------
.../EntityWithNullableDictionary.cs | 5 +++
.../BuildersGeneratorNullableTests.cs | 31 +++++++++++++
6 files changed, 93 insertions(+), 37 deletions(-)
diff --git a/Buildenator/CodeAnalysis/ITypedSymbol.cs b/Buildenator/CodeAnalysis/ITypedSymbol.cs
index f7f4940..acce241 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; }
@@ -32,12 +38,6 @@ internal interface ITypedSymbol
///
/// The default value name (e.g., "DefaultName") if found, otherwise null.
string? GetDefaultValueName();
-
- ///
- /// Returns the type full name suitable for use as a generic type parameter.
- /// For nullable reference types, strips the nullable annotation to avoid CS8628 errors.
- ///
- string GetTypeFullNameForGenericParameter();
}
///
diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs
index ab41818..863053a 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();
@@ -135,12 +164,12 @@ public string GenerateFieldType()
}
public string GenerateLazyFieldType()
- => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{GetTypeFullNameForGenericParameter()}>?";
+ => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{NonNullableTypeFullName}>?";
public string GenerateLazyFieldValueReturn()
=> IsMockable()
? string.Format(_mockingProperties!.ReturnObjectFormat, UnderScoreName)
- : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{GetTypeFullNameForGenericParameter()}>({(IsFakeable()
+ : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{NonNullableTypeFullName}>({(IsFakeable()
? $"{string.Format(_fixtureProperties!.CreateSingleFormat, TypeFullName, SymbolName, DefaultConstants.FixtureLiteral)}"
+ (_nullableStrategy == NullableStrategy.Enabled ? "!" : "")
: $"default({TypeFullName})")})).Object";
@@ -153,16 +182,6 @@ public string GenerateFieldValueReturn()
public string GenerateMethodParameterDefinition()
=> IsMockable() ? $"Action<{GenerateMockableFieldType()}> {DefaultConstants.SetupActionLiteral}" : $"{TypeFullName} {DefaultConstants.ValueLiteral}";
- ///
- /// Returns the type full name suitable for use as a generic type parameter.
- /// For nullable reference types, strips the nullable annotation to avoid CS8628 errors.
- /// For example, "Dictionary<string, string>?" becomes "Dictionary<string, string>".
- ///
- public string GetTypeFullNameForGenericParameter()
- => Type.NullableAnnotation == NullableAnnotation.Annotated
- ? Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString()
- : TypeFullName;
-
public string? GetDefaultValueName()
{
if (_defaultValueNames == null)
diff --git a/Buildenator/Generators/ConstructorsGenerator.cs b/Buildenator/Generators/ConstructorsGenerator.cs
index 0d72ca9..13d7345 100644
--- a/Buildenator/Generators/ConstructorsGenerator.cs
+++ b/Buildenator/Generators/ConstructorsGenerator.cs
@@ -83,19 +83,19 @@ private static bool ShouldInitializeCollectionField(ITypedSymbol typedSymbol)
private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSymbol, CollectionMetadata collectionMetadata)
{
var fieldName = typedSymbol.UnderScoreName;
- var typeForGenericParam = typedSymbol.GetTypeFullNameForGenericParameter();
+ var typeFullName = typedSymbol.NonNullableTypeFullName;
// For concrete dictionary types, create new instance
if (collectionMetadata is ConcreteDictionaryMetadata)
{
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {typeForGenericParam}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {typeFullName}());";
}
// For interface dictionary types, create a Dictionary
if (collectionMetadata is InterfaceDictionaryMetadata dictMetadata)
{
var dictionaryType = $"System.Collections.Generic.Dictionary<{dictMetadata.KeyTypeDisplayName}, {dictMetadata.ValueTypeDisplayName}>";
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {dictionaryType}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {dictionaryType}());";
}
// For concrete collection types, create new instance.
@@ -103,7 +103,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}<{typeForGenericParam}>(new {typeForGenericParam}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {typeFullName}());";
}
// For interface collection types, create a List
@@ -111,7 +111,7 @@ private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSy
{
var elementTypeName = collectionMetadata.ElementTypeDisplayName;
var listType = $"System.Collections.Generic.List<{elementTypeName}>";
- return $"{fieldName} = new {DefaultConstants.NullBox}<{typeForGenericParam}>(new {listType}());";
+ return $"{fieldName} = new {DefaultConstants.NullBox}<{typeFullName}>(new {listType}());";
}
return string.Empty;
diff --git a/Buildenator/Generators/PropertiesStringGenerator.cs b/Buildenator/Generators/PropertiesStringGenerator.cs
index 54b11e4..e18d839 100644
--- a/Buildenator/Generators/PropertiesStringGenerator.cs
+++ b/Buildenator/Generators/PropertiesStringGenerator.cs
@@ -175,7 +175,7 @@ private string GenerateFieldInitializer(ITypedSymbol typedSymbol)
if (defaultValueName is null)
return string.Empty;
- return $" = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>({defaultValueName})";
+ return $" = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({defaultValueName})";
}
private string GenerateMethodDefinition(ITypedSymbol typedSymbol)
@@ -193,9 +193,10 @@ private static string GenerateValueAssignment(ITypedSymbol typedSymbol)
if (typedSymbol.IsMockable())
return $"{DefaultConstants.SetupActionLiteral}({typedSymbol.UnderScoreName})";
- // Add null-forgiving operator when the parameter is nullable but NullBox expects non-nullable
- var nullForgiving = typedSymbol.TypeFullName != typedSymbol.GetTypeFullNameForGenericParameter() ? "!" : "";
- return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>({DefaultConstants.ValueLiteral}{nullForgiving})";
+ // Add null-forgiving operator (!) for nullable reference types to suppress CS8604 warnings
+ // when assigning potentially null values to NullBox where T is non-nullable
+ var nullForgiving = typedSymbol.TypeFullName != typedSymbol.NonNullableTypeFullName ? "!" : "";
+ return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({DefaultConstants.ValueLiteral}{nullForgiving})";
}
private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}";
@@ -224,7 +225,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- dictionary = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
+ dictionary = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var item in items)
@@ -232,7 +233,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
return this;
}}";
}
@@ -252,7 +253,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
{{
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
return this;
}}";
}
@@ -269,7 +270,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
}}
else
{{
- collection = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
+ collection = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var item in items)
@@ -277,7 +278,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
collection.Add(item);
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
return this;
}}";
}
@@ -289,7 +290,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object)
: new System.Collections.Generic.List<{elementTypeName}>();
list.AddRange(items);
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(({typedSymbol.GetTypeFullNameForGenericParameter()})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
return this;
}}";
}
@@ -320,7 +321,7 @@ private string GenerateChildBuilderMethodDefinition(ITypedSymbol typedSymbol)
var methodName = CreateMethodName(typedSymbol);
var fieldName = typedSymbol.UnderScoreName;
- var entityTypeName = typedSymbol.GetTypeFullNameForGenericParameter();
+ var entityTypeName = typedSymbol.NonNullableTypeFullName;
return $@"public {_builder.FullName} {methodName}(System.Func<{childBuilderName}, {childBuilderName}> configure{typedSymbol.SymbolPascalName})
{{
@@ -352,7 +353,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
}}
else
{{
- collection = new {typedSymbol.GetTypeFullNameForGenericParameter()}();
+ collection = new {typedSymbol.NonNullableTypeFullName}();
}}
foreach (var configure in configures)
@@ -362,7 +363,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
collection.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
return this;
}}";
}
@@ -381,7 +382,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
list.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.GetTypeFullNameForGenericParameter()}>(({typedSymbol.GetTypeFullNameForGenericParameter()})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
return this;
}}";
}
diff --git a/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
index a36a9be..68da425 100644
--- a/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
+++ b/Tests/Buildenator.IntegrationTests.SharedEntitiesNullable/EntityWithNullableDictionary.cs
@@ -32,5 +32,10 @@ public class EntityWithNullableDictionary
/// 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/BuildersGeneratorNullableTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
index b160562..738eb6d 100644
--- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
+++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs
@@ -332,5 +332,36 @@ public void BuildersGenerator_NullableDictionary_AddToMethodShouldAddItems(strin
_ = 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();
+ }
}
}
From 8346b4ad66066cd7250292bfcf2e7967b1ef6d6f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 12:43:00 +0000
Subject: [PATCH 8/8] Use NonNullableTypeFullName only for new instantiations,
not NullBox generic parameters
Co-authored-by: pmrogala <9459432+pmrogala@users.noreply.github.com>
---
Buildenator/CodeAnalysis/TypedSymbol.cs | 4 ++--
.../Generators/ConstructorsGenerator.cs | 7 +++---
.../Generators/PropertiesStringGenerator.cs | 22 ++++++++-----------
3 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/Buildenator/CodeAnalysis/TypedSymbol.cs b/Buildenator/CodeAnalysis/TypedSymbol.cs
index 863053a..bb620de 100644
--- a/Buildenator/CodeAnalysis/TypedSymbol.cs
+++ b/Buildenator/CodeAnalysis/TypedSymbol.cs
@@ -164,12 +164,12 @@ public string GenerateFieldType()
}
public string GenerateLazyFieldType()
- => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{NonNullableTypeFullName}>?";
+ => IsMockable() ? GenerateMockableFieldType() : $"{DefaultConstants.NullBox}<{TypeFullName}>?";
public string GenerateLazyFieldValueReturn()
=> IsMockable()
? string.Format(_mockingProperties!.ReturnObjectFormat, UnderScoreName)
- : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{NonNullableTypeFullName}>({(IsFakeable()
+ : @$"({UnderScoreName}.HasValue ? {UnderScoreName}.Value : new {DefaultConstants.NullBox}<{TypeFullName}>({(IsFakeable()
? $"{string.Format(_fixtureProperties!.CreateSingleFormat, TypeFullName, SymbolName, DefaultConstants.FixtureLiteral)}"
+ (_nullableStrategy == NullableStrategy.Enabled ? "!" : "")
: $"default({TypeFullName})")})).Object";
diff --git a/Buildenator/Generators/ConstructorsGenerator.cs b/Buildenator/Generators/ConstructorsGenerator.cs
index 13d7345..19c7dc5 100644
--- a/Buildenator/Generators/ConstructorsGenerator.cs
+++ b/Buildenator/Generators/ConstructorsGenerator.cs
@@ -83,12 +83,13 @@ private static bool ShouldInitializeCollectionField(ITypedSymbol typedSymbol)
private static string GenerateEmptyCollectionInitialization(ITypedSymbol typedSymbol, CollectionMetadata collectionMetadata)
{
var fieldName = typedSymbol.UnderScoreName;
- var typeFullName = typedSymbol.NonNullableTypeFullName;
+ 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 e18d839..ea5bcba 100644
--- a/Buildenator/Generators/PropertiesStringGenerator.cs
+++ b/Buildenator/Generators/PropertiesStringGenerator.cs
@@ -175,7 +175,7 @@ private string GenerateFieldInitializer(ITypedSymbol typedSymbol)
if (defaultValueName is null)
return string.Empty;
- return $" = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({defaultValueName})";
+ return $" = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({defaultValueName})";
}
private string GenerateMethodDefinition(ITypedSymbol typedSymbol)
@@ -193,10 +193,7 @@ private static string GenerateValueAssignment(ITypedSymbol typedSymbol)
if (typedSymbol.IsMockable())
return $"{DefaultConstants.SetupActionLiteral}({typedSymbol.UnderScoreName})";
- // Add null-forgiving operator (!) for nullable reference types to suppress CS8604 warnings
- // when assigning potentially null values to NullBox where T is non-nullable
- var nullForgiving = typedSymbol.TypeFullName != typedSymbol.NonNullableTypeFullName ? "!" : "";
- return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>({DefaultConstants.ValueLiteral}{nullForgiving})";
+ return $"{typedSymbol.UnderScoreName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({DefaultConstants.ValueLiteral})";
}
private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}";
@@ -233,7 +230,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary);
return this;
}}";
}
@@ -253,7 +250,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
{{
dictionary[item.Key] = item.Value;
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(dictionary);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary);
return this;
}}";
}
@@ -278,7 +275,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
collection.Add(item);
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection);
return this;
}}";
}
@@ -290,7 +287,7 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol)
? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object)
: new System.Collections.Generic.List<{elementTypeName}>();
list.AddRange(items);
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(({typedSymbol.TypeFullName})list);
return this;
}}";
}
@@ -321,13 +318,12 @@ private string GenerateChildBuilderMethodDefinition(ITypedSymbol typedSymbol)
var methodName = CreateMethodName(typedSymbol);
var fieldName = typedSymbol.UnderScoreName;
- var entityTypeName = typedSymbol.NonNullableTypeFullName;
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;
}}";
}
@@ -363,7 +359,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
collection.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(collection);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(collection);
return this;
}}";
}
@@ -382,7 +378,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo
list.Add(childBuilder.Build());
}}
- {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.NonNullableTypeFullName}>(({typedSymbol.NonNullableTypeFullName})list);
+ {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(({typedSymbol.TypeFullName})list);
return this;
}}";
}