Skip to content

Commit 5b9eb81

Browse files
committed
DummyBuilder records clone themselves on each step, for reuse.
1 parent 8d46796 commit 5b9eb81

File tree

4 files changed

+34
-7
lines changed

4 files changed

+34
-7
lines changed

DomainModeling.Generator/DummyBuilderGenerator.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,15 @@ namespace {containingNamespace}
297297
/// That way, if the constructor changes, only the builder needs to be adjusted, rather than lots of test methods.
298298
/// </para>
299299
/// </summary>
300-
[CompilerGenerated] {type.DeclaredAccessibility.ToCodeString()} partial{(builder.IsRecord ? " record" : "")} class {typeName}
300+
[CompilerGenerated] {type.DeclaredAccessibility.ToCodeString()} partial {(builder.IsRecord ? "record " : "")}class {typeName}
301301
{{
302302
{joinedComponents}
303303
304304
private {typeName} With(Action<{typeName}> assignment)
305305
{{
306-
assignment(this);
307-
return this;
306+
var instance = this{(builder.IsRecord ? " with { }" : "")}; // If the type is a record, a copy is made, to enable reuse per step
307+
assignment(instance);
308+
return instance;
308309
}}
309310
310311
{(hasBuildMethod ? "/*" : "")}

DomainModeling.Tests/DummyBuilderTests.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ public void Build_Regularly_ShouldReturnExpectedResult()
1919
Assert.Equal(1m, result.Amount.Amount.Value);
2020
}
2121

22+
[Fact]
23+
public void Build_WithReuseOfRecordTypedBuilder_ShouldReturnExpectedResult()
24+
{
25+
var builder = new TestEntityDummyBuilder()
26+
.WithCount(5);
27+
28+
var result1 = builder
29+
.WithCreationDate(DateOnly.FromDateTime(DateTime.UnixEpoch))
30+
.Build();
31+
32+
var result2 = builder
33+
.WithModificationDateTime(DateTime.UnixEpoch)
34+
.Build();
35+
36+
Assert.Equal(5, result1.Count);
37+
Assert.Equal(DateOnly.FromDateTime(DateTime.UnixEpoch), result1.CreationDate);
38+
Assert.NotEqual(DateTime.UnixEpoch, result1.ModificationDateTime);
39+
40+
Assert.Equal(5, result2.Count);
41+
Assert.NotEqual(DateOnly.FromDateTime(DateTime.UnixEpoch), result2.CreationDate);
42+
Assert.Equal(DateTime.UnixEpoch, result2.ModificationDateTime);
43+
}
44+
2245
[Fact]
2346
public void Build_WithCustomizations_ShouldReturnExpectedResult()
2447
{
@@ -62,7 +85,7 @@ public void Build_WithStringWrapperValueObject_ShouldUseEntityConstructorParamet
6285
namespace DummyBuilderTestTypes
6386
{
6487
[DummyBuilder<TestEntity>]
65-
public sealed partial class TestEntityDummyBuilder
88+
public sealed partial record class TestEntityDummyBuilder
6689
{
6790
// Demonstrate that we can take priority over the generated members
6891
public TestEntityDummyBuilder WithCreationDateTime(DateTime value) => this.With(b => b.CreationDateTime = value);

DomainModeling/DomainModeling.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Misc:
6565
- Semi-breaking: IFormattable &amp; co for string wrappers have stopped treating null strings as "", as this could cover up mistakes instead of revealing them.
6666
- Semi-breaking: IIdentity now implements IWrapperValueObject.
6767
- Feature: Non-generic Wrapper/Identity interfaces.
68+
- Feature: DummyBuilder records clone themselves on each step, for reuse.
6869
- Feature: Analyzer warns when '==' or similar operator implicitly casts some IValueObject to something else. This avoids accidentally comparing unrelated types.
6970
- Fix: Fixed bug where source-generated records would always generate ToString()/Equals()/GetHashCode(), even if you wrote your own.
7071
- Fix: Fixed bug where source-generated Wrappers/Identities would not recognize manual member implementations if they were explicit interface implementations.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ The simple act of adding one property would require dozens of additional changes
227227
The Builder pattern fixes this problem:
228228

229229
```cs
230-
public class PaymentDummyBuilder
230+
public record class PaymentDummyBuilder
231231
{
232232
// Have a default value for each property, along with a fluent method to change it
233233
@@ -255,7 +255,7 @@ public class PaymentDummyBuilder
255255
}
256256
```
257257

258-
Test methods avoid constructor invocations, e.g. `new Payment("EUR", 1.00m)`, and instead use the following:
258+
Test methods can then avoid constructor invocations, e.g. `new Payment("EUR", 1.00m)`, and instead use the following:
259259

260260
```cs
261261
new PaymentBuilder().Build(); // Completely default instance
@@ -278,7 +278,7 @@ Change the type as follows to get source generation for it:
278278

279279
```cs
280280
[DummyBuilder<Payment>]
281-
public partial class PaymentDummyBuilder
281+
public partial record class PaymentDummyBuilder
282282
{
283283
// Anything defined manually will cause the source generator to outcomment its conflicting code, i.e. manual code always takes precedence
284284
@@ -294,6 +294,8 @@ The generated `Build()` method opts for _the most visible, simplest parameterize
294294

295295
Dummy builders generally live in a test project, or in a library project consumed solely by test projects.
296296

297+
Note that, if the dummy builder is a record class, a new copy is made on every mutation. This allows a partially constructed builder to be reused in multiple directions.
298+
297299
## Constructor Validation
298300

299301
DDD promotes the validation of domain rules and invariants in the constructors of the domain objects. This pattern is fully supported:

0 commit comments

Comments
 (0)