Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Buildenator.Abstraction/BuildenatorConfigurationAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public sealed class BuildenatorConfigurationAttribute : Attribute
/// <param name="generateStaticPropertyForBuilderCreation">If you want to generate static property that will return a new builder instance.</param>
/// <param name="initializeCollectionsWithEmpty">If true, collection fields will be initialized with empty collections in the constructor instead of null.</param>
/// <param name="useChildBuilders">If true, generates additional With methods that accept Func&lt;ChildBuilder, ChildBuilder&gt; for properties that have their own builders.</param>
/// <param name="builderConstructorMandatoryParameters">If true, all generated builders in this assembly will require the entity's constructor parameters as mandatory builder constructor arguments. Incompatible with mocking.</param>
public BuildenatorConfigurationAttribute(
string buildingMethodsPrefix = "With",
bool generateDefaultBuildMethod = true,
Expand All @@ -25,7 +26,8 @@ public BuildenatorConfigurationAttribute(
bool implicitCast = false,
bool generateStaticPropertyForBuilderCreation = false,
bool initializeCollectionsWithEmpty = true,
bool useChildBuilders = true)
bool useChildBuilders = true,
bool builderConstructorMandatoryParameters = false)
{
}
}
4 changes: 3 additions & 1 deletion Buildenator.Abstraction/MakeBuilderAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed class MakeBuilderAttribute : Attribute
/// <param name="generateStaticPropertyForBuilderCreation">If you want to generate static property that will return a new builder instance.</param>
/// <param name="initializeCollectionsWithEmpty">If true, collection fields will be initialized with empty collections in the constructor instead of null.</param>
/// <param name="useChildBuilders">If true, generates additional With methods that accept Func&lt;ChildBuilder, ChildBuilder&gt; for properties that have their own builders.</param>
/// <param name="builderConstructorMandatoryParameters">If true, the generated builder constructor will require the entity's constructor parameters as mandatory arguments instead of using a parameterless constructor. Incompatible with mocking.</param>
public MakeBuilderAttribute(
Type typeForBuilder,
string? buildingMethodsPrefix = "With",
Expand All @@ -29,7 +30,8 @@ public MakeBuilderAttribute(
string? staticFactoryMethodName = null,
object? generateStaticPropertyForBuilderCreation = null,
object? initializeCollectionsWithEmpty = null,
object? useChildBuilders = null
object? useChildBuilders = null,
object? builderConstructorMandatoryParameters = null
)
{
}
Expand Down
4 changes: 3 additions & 1 deletion Buildenator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ BDN002 | Buildenator | Error | BuildersGenerator
BDN003 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN004 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN005 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN006 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN006 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN007 | Buildenator | Error | BuildenatorDiagnosticDescriptors
BDN008 | Buildenator | Error | BuildenatorDiagnosticDescriptors
2 changes: 1 addition & 1 deletion Buildenator/Buildenator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<!-- Do not include the generator as a lib dependency -->
<Version>8.5.0.2</Version>
<Version>8.8.0.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
7 changes: 6 additions & 1 deletion Buildenator/Configuration/BuilderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static BuilderProperties Create(
bool? generateStaticPropertyForBuilderCreation = null;
bool? initializeCollectionsWithEmpty = null;
bool? useChildBuilders = null;
bool? builderConstructorMandatoryParameters = null;

if (globalAttributes.HasValue)
{
Expand All @@ -42,6 +43,7 @@ public static BuilderProperties Create(
generateStaticPropertyForBuilderCreation = globalAttributes.Value.GetOrThrow<bool>(5, nameof(MakeBuilderAttributeInternal.GenerateStaticPropertyForBuilderCreation));
initializeCollectionsWithEmpty = globalAttributes.Value.GetOrThrow<bool>(6, nameof(MakeBuilderAttributeInternal.InitializeCollectionsWithEmpty));
useChildBuilders = globalAttributes.Value.GetOrThrow<bool>(7, nameof(MakeBuilderAttributeInternal.UseChildBuilders));
builderConstructorMandatoryParameters = globalAttributes.Value.GetOrThrow<bool>(8, nameof(MakeBuilderAttributeInternal.BuilderConstructorMandatoryParameters));
}

nullableStrategy = builderAttribute.NullableStrategy is null ? nullableStrategy: builderAttribute.NullableStrategy;
Expand All @@ -64,7 +66,8 @@ public static BuilderProperties Create(
builderAttribute.StaticFactoryMethodName,
builderAttribute.GenerateStaticPropertyForBuilderCreation ?? generateStaticPropertyForBuilderCreation,
builderAttribute.InitializeCollectionsWithEmpty ?? initializeCollectionsWithEmpty,
builderAttribute.UseChildBuilders ?? useChildBuilders));
builderAttribute.UseChildBuilders ?? useChildBuilders,
builderAttribute.BuilderConstructorMandatoryParameters ?? builderConstructorMandatoryParameters));
}

private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttributeInternal attributeData)
Expand All @@ -82,6 +85,7 @@ private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttri
GenerateStaticPropertyForBuilderCreation = attributeData.GenerateStaticPropertyForBuilderCreation ?? false;
InitializeCollectionsWithEmpty = attributeData.InitializeCollectionsWithEmpty ?? true;
UseChildBuilders = attributeData.UseChildBuilders ?? true;
BuilderConstructorMandatoryParameters = attributeData.BuilderConstructorMandatoryParameters ?? false;

if (string.IsNullOrWhiteSpace(BuildingMethodsPrefix))
throw new ArgumentNullException(nameof(attributeData), "Prefix name shouldn't be empty!");
Expand Down Expand Up @@ -182,6 +186,7 @@ private static bool IsAccessibleDefaultValueMember(bool isStatic, Accessibility
public bool GenerateStaticPropertyForBuilderCreation { get; }
public bool InitializeCollectionsWithEmpty { get; }
public bool UseChildBuilders { get; }
public bool BuilderConstructorMandatoryParameters { get; }

public IReadOnlyDictionary<string, List<IMethodSymbol>> BuildingMethods => _buildingMethods;
public IReadOnlyDictionary<string, IFieldSymbol> Fields => _fields;
Expand Down
1 change: 1 addition & 0 deletions Buildenator/Configuration/Contract/IBuilderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal interface IBuilderProperties
bool GenerateStaticPropertyForBuilderCreation { get; }
bool InitializeCollectionsWithEmpty { get; }
bool UseChildBuilders { get; }
bool BuilderConstructorMandatoryParameters { get; }

/// <summary>
/// Gets the set of user-defined default value names available in this builder.
Expand Down
16 changes: 16 additions & 0 deletions Buildenator/Diagnostics/BuildenatorDiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,20 @@ internal static class BuildenatorDiagnosticDescriptors
"Buildenator",
DiagnosticSeverity.Info,
true);

internal static readonly DiagnosticDescriptor MandatoryParametersWithMockingDiagnostic = new(
"BDN007",
"BuilderConstructorMandatoryParameters is incompatible with mocking",
"The builder '{0}' has BuilderConstructorMandatoryParameters = true, which is incompatible with a mocking configuration. Remove the mocking configuration or disable BuilderConstructorMandatoryParameters.",
"Buildenator",
DiagnosticSeverity.Error,
true);

internal static readonly DiagnosticDescriptor MandatoryParametersWithFixtureDiagnostic = new(
"BDN008",
"BuilderConstructorMandatoryParameters is incompatible with fixture configuration",
"The builder '{0}' has BuilderConstructorMandatoryParameters = true, which is incompatible with a fixture configuration. Remove the fixture configuration or disable BuilderConstructorMandatoryParameters.",
"Buildenator",
DiagnosticSeverity.Error,
true);
}
18 changes: 16 additions & 2 deletions Buildenator/Generators/BuilderSourceStringGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ public BuilderSourceStringGenerator(
{
_diagnostics.Add(privateConstructorDiagnostic);
}
if (_builder.BuilderConstructorMandatoryParameters && mockingConfiguration != null)
{
_diagnostics.Add(new BuildenatorDiagnostic(
BuildenatorDiagnosticDescriptors.MandatoryParametersWithMockingDiagnostic,
_builder.OriginalLocation,
_builder.Name));
}
if (_builder.BuilderConstructorMandatoryParameters && fixtureConfiguration != null)
{
_diagnostics.Add(new BuildenatorDiagnostic(
BuildenatorDiagnosticDescriptors.MandatoryParametersWithFixtureDiagnostic,
_builder.OriginalLocation,
_builder.Name));
}
_diagnostics.AddRange(_builder.Diagnostics);
_diagnostics.AddRange(_entity.Diagnostics);

Expand Down Expand Up @@ -67,11 +81,11 @@ namespace {_builder.ContainingNamespace}
{GenerateGlobalNullable()}{GenerateBuilderDefinition()}
{{
{(_fixtureConfiguration is null ? string.Empty : $" private readonly {_fixtureConfiguration.Name} {DefaultConstants.FixtureLiteral} = new {_fixtureConfiguration.Name}({_fixtureConfiguration.ConstructorParameters});")}
{(_builder.IsDefaultConstructorOverriden ? string.Empty : GenerateConstructor(_builder.Name, _entity, _fixtureConfiguration, _builder.InitializeCollectionsWithEmpty))}
{(_builder.IsDefaultConstructorOverriden ? string.Empty : GenerateConstructor(_builder.Name, _entity, _fixtureConfiguration, _builder.InitializeCollectionsWithEmpty, _builder.BuilderConstructorMandatoryParameters))}
{_propertiesStringGenerator.GeneratePropertiesCode()}
{(_builder.IsBuildMethodOverriden ? string.Empty : _entity.GenerateBuildsCode(_builder.ShouldGenerateMethodsForUnreachableProperties))}
{(_builder.IsBuildManyMethodOverriden ? string.Empty : GenerateBuildManyCode())}
{(_builder.GenerateStaticPropertyForBuilderCreation ? $" public static {_builder.FullName} {_entity.Name} => new {_builder.FullName}();" : "")}
{(_builder.GenerateStaticPropertyForBuilderCreation && !_builder.BuilderConstructorMandatoryParameters ? $" public static {_builder.FullName} {_entity.Name} => new {_builder.FullName}();" : "")}
{(_builder.GenerateDefaultBuildMethod ? _entity.GenerateDefaultBuildsCode() : string.Empty)}
{(_builder.ImplicitCast ? GenerateImplicitCastCode() : string.Empty)}
{GeneratePreBuildMethod()}
Expand Down
54 changes: 54 additions & 0 deletions Buildenator/Generators/ConstructorsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,60 @@ namespace Buildenator.Generators;
internal static class ConstructorsGenerator
{
internal static string GenerateConstructor(
string builderName,
IEntityToBuild entity,
IFixtureProperties? fixtureConfiguration,
bool initializeCollectionsWithEmpty,
bool builderConstructorMandatoryParameters)
{
return builderConstructorMandatoryParameters
? GenerateMandatoryParamConstructor(builderName, entity, initializeCollectionsWithEmpty)
: GenerateParameterlessConstructor(builderName, entity, fixtureConfiguration, initializeCollectionsWithEmpty);
Comment thread
pmrogala marked this conversation as resolved.
}

private static string GenerateMandatoryParamConstructor(
string builderName,
IEntityToBuild entity,
bool initializeCollectionsWithEmpty)
{
var constructorToBuild = entity.ConstructorToBuild;
var mandatoryParams = constructorToBuild?.Parameters.ToList() ?? [];

var signatureParams = string.Join(", ", mandatoryParams.Select(p => $"{p.TypeFullName} {p.SymbolName}"));

var output = new StringBuilder();
output.AppendLine($@"{CommentsGenerator.GenerateSummaryOverrideComment()}
public {builderName}({signatureParams})
{{");

foreach (var p in mandatoryParams)
{
output.AppendLine($@" {p.UnderScoreName} = new {DefaultConstants.NullBox}<{p.TypeFullName}>({p.SymbolName});");
}

var settableProperties = entity.AllUniqueSettablePropertiesAndParameters
.Where(p => constructorToBuild?.ContainsParameter(p.SymbolPascalName) != true)
.ToList();

if (initializeCollectionsWithEmpty)
{
foreach (var typedSymbol in settableProperties.Where(ShouldInitializeCollectionField))
{
var collectionMetadata = typedSymbol.GetCollectionMetadata();
if (collectionMetadata != null)
{
var initCode = GenerateEmptyCollectionInitialization(typedSymbol, collectionMetadata);
if (!string.IsNullOrEmpty(initCode))
output.AppendLine($@" {initCode}");
}
}
}

output.AppendLine($@" }}");
return output.ToString();
}

private static string GenerateParameterlessConstructor(
string builderName,
IEntityToBuild entity,
IFixtureProperties? fixtureConfiguration,
Expand Down
7 changes: 5 additions & 2 deletions Buildenator/MakeBuilderAttributeInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ internal readonly struct MakeBuilderAttributeInternal(
string? staticFactoryMethodName,
bool? generateStaticPropertyForBuilderCreation,
bool? initializeCollectionsWithEmpty,
bool? useChildBuilders)
bool? useChildBuilders,
bool? builderConstructorMandatoryParameters)
{

public MakeBuilderAttributeInternal(AttributeData attribute)
Expand All @@ -29,7 +30,8 @@ attribute.ConstructorArguments[3].Value is null
(string?)attribute.ConstructorArguments[6].Value,
(bool?)attribute.ConstructorArguments[7].Value,
(bool?)attribute.ConstructorArguments[8].Value,
(bool?)attribute.ConstructorArguments[9].Value)
(bool?)attribute.ConstructorArguments[9].Value,
(bool?)attribute.ConstructorArguments[10].Value)
{

}
Expand All @@ -43,5 +45,6 @@ attribute.ConstructorArguments[3].Value is null
public bool? GenerateStaticPropertyForBuilderCreation { get; } = generateStaticPropertyForBuilderCreation;
public bool? InitializeCollectionsWithEmpty { get; } = initializeCollectionsWithEmpty;
public bool? UseChildBuilders { get; } = useChildBuilders;
public bool? BuilderConstructorMandatoryParameters { get; } = builderConstructorMandatoryParameters;
internal string? StaticFactoryMethodName { get; } = staticFactoryMethodName;
}
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ 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.8.0.0 - 2026-3-6

### Added
- **`builderConstructorMandatoryParameters` option**: New configuration option that makes the generated builder enforce mandatory parameters by requiring the entity's constructor parameters as arguments to the builder's own constructor.
- Enabled per builder: `[MakeBuilder(typeof(MyClass), builderConstructorMandatoryParameters: true)]`
- Enabled globally: `[assembly: BuildenatorConfiguration(builderConstructorMandatoryParameters: true)]`
- When enabled:
- The builder gets a **parameterized constructor** mirroring the entity's primary constructor instead of a parameterless one.
- The **static factory property** (`generateStaticPropertyForBuilderCreation`) is automatically suppressed, since it would call a parameterless constructor that no longer exists.
- All `With...` methods remain available and can still override the values passed to the constructor.
- **Mocking is incompatible** — combining this option with a mocking configuration (e.g. `MoqConfiguration`) produces a compile-time error **BDN007**.
- **Fixture configuration is incompatible** — combining this option with a fixture configuration (e.g. `AutoFixtureConfiguration`) produces a compile-time error **BDN008**.
- Example:
```csharp
public class Dto
{
public Dto(int lineNumber, string activity) { LineNumber = lineNumber; Activity = activity; }
public int LineNumber { get; set; }
public string Activity { get; set; }
public List<string> Tags { get; set; } = [];
}

[MakeBuilder(typeof(Dto), builderConstructorMandatoryParameters: true)]
public partial class DtoBuilder { }

// Usage — constructor enforces required values, With methods allow overrides
var dto = new DtoBuilder(lineNumber: 1, activity: "shipping")
.AddToTags("express")
.Build();
```
- **BDN007 diagnostic error**: Emitted when `builderConstructorMandatoryParameters = true` is combined with a mocking configuration, since the two features are fundamentally incompatible.
- **BDN008 diagnostic error**: Emitted when `builderConstructorMandatoryParameters = true` is combined with a fixture configuration, since fixture auto-generation of unset values is incompatible with explicit constructor-driven initialization.

## 8.7.1.0 - 2025-12-4

### Fixed
Expand Down
Loading
Loading