Skip to content

Commit

Permalink
Adding AfterModel action for two sided relationships
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphael Hoppe committed Oct 22, 2024
1 parent c367f97 commit 41b1d4b
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Company>queo GmbH</Company>
<Copyright>2023 queo GmbH</Copyright>
<Product>Queo Commons ModelBuilder Library</Product>
<AssemblyVersion>0.1.0</AssemblyVersion>
<AssemblyVersion>0.1.1</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<version>$(AssemblyVersion)-Beta</version>
</PropertyGroup>
Expand Down
21 changes: 20 additions & 1 deletion src/Commons.Builders.Model/Builder/ModelBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

Expand Down Expand Up @@ -64,6 +64,7 @@ public TModel Build()
{
_factory.PreBuild.Execute<TModel>(this);
_model = BuildModel();
AfterModel(_model);
_factory.PostBuild.Execute<TModel>(_model!);
}
return _model;
Expand All @@ -74,6 +75,24 @@ public TModel Build()
/// </summary>
protected abstract TModel BuildModel();


/// <summary>
/// Method that can be overwritten for common 'Chicken and egg' situations during setup
/// This will run before the factories post actions are executed, but after the model is build.
/// This allows us to resolve a dependency of a child builder, without running into a cyclic call.
///
/// The way this works is, the parent will get build, without the children connected,
/// and then the children will be built in this method and then connected to the parent.
///
/// model = Parent.Build();
/// child = _childBuilder.WithParent(Parent).Build();
/// model.Add(child);
/// </summary>
protected virtual void AfterModel(TModel model)
{
// intentionally empty, since this is a optional action, to be overwritten by specific builders
}

/// <summary>
/// Validation if the builder is locked or not.
/// This method should be called in every builder method, to warn the user early
Expand Down
59 changes: 59 additions & 0 deletions tests/Commons.Builders.Model.Tests/AfterModel/AfterModelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using NUnit.Framework;

using FluentAssertions;

using Queo.Commons.Builders.Model.Examples;
using Queo.Commons.Builders.Model.Examples.Relations;

namespace Queo.Commons.Builders.Model.Tests.AfterModel;

[TestFixture]
public class AfterModelTests
{
[Test]
public void CreateCountry_WithPresident()
{
Country c = Create.Country().WithName("USA")
.WithPresident(p => p.WithName("Roosevelt"));

c.Name.Should().Be("USA");
c.President?.Name.Should().Be("Roosevelt");
c.Should().Be(c.President?.Country);
}

[Test]
public void CreatePresident_WithCountry()
{
President p = Create.President().WithName("Roosevelt")
.WithCountry(c => c.WithName("USA"));

p.Name.Should().Be("Roosevelt");
p.Country.Name.Should().Be("USA");
p.Country.President.Should().Be(p);
}

[Test]
public void CreateUser_WithOrg()
{
User u = Create.User().WithName("George")
.WithOrg(o => o.WithName("GOrg"));

u.Name.Should().Be("George");
u.Org?.Name.Should().Be("GOrg");
u.Org?.Members.Should().Contain(u);
}

[Test]
public void CreateOrg_WithUsers()
{
Org o = Create.Org().WithName("GOrg")
.WithAdmin(u => u.WithName("George"))
.AddMember(u => u.WithName("Other"));

o.Name.Should().Be("GOrg");
o.Admin.Name.Should().Be("George");
o.Admin.Org.Should().Be(o);
o.Members.Should().Contain(m => m.Name.Equals("Other"));
o.Members.Should().Contain(m => m.Name.Equals("George"));
}
}
7 changes: 7 additions & 0 deletions tests/Examples/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Queo.Commons.Builders.Model.Examples.DAG;
using Queo.Commons.Builders.Model.Examples.Person;
using Queo.Commons.Builders.Model.Examples.Person.Mocks;
using Queo.Commons.Builders.Model.Examples.Relations;
using Queo.Commons.Builders.Model.Examples.Tree;
using Queo.Commons.Builders.Model.Factory;

Expand All @@ -25,5 +26,11 @@ public static class Create

public static PersonBuilder Person() => Factory.Create<PersonBuilder>();
public static PersonGeneratorBuilder GeneratedPerson() => GeneratorFactory.Create<PersonGeneratorBuilder>();

public static CountryBuilder Country() => Factory.Create<CountryBuilder>();
public static PresidentBuilder President() => Factory.Create<PresidentBuilder>();

public static UserBuilder User() => Factory.Create<UserBuilder>();
public static OrgBuilder Org() => Factory.Create<OrgBuilder>();
}
}
5 changes: 5 additions & 0 deletions tests/Examples/ExampleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Queo.Commons.Builders.Model.Examples.Car.Builders;
using Queo.Commons.Builders.Model.Examples.DAG;
using Queo.Commons.Builders.Model.Examples.Person;
using Queo.Commons.Builders.Model.Examples.Relations;
using Queo.Commons.Builders.Model.Examples.Tree;
using Queo.Commons.Builders.Model.Factory;

Expand Down Expand Up @@ -33,6 +34,10 @@ public TBuilder Create<TBuilder>()
Type car when car == typeof(CarBuilder) => new CarBuilder(this),
Type proxy when proxy == typeof(ProxyBuilder) => new ProxyBuilder(this),
Type wheel when wheel == typeof(WheelBuilder) => new WheelBuilder(this),
Type t when t == typeof(CountryBuilder) => new CountryBuilder(this),
Type t when t == typeof(PresidentBuilder) => new PresidentBuilder(this),
Type t when t == typeof(UserBuilder) => new UserBuilder(this),
Type t when t == typeof(OrgBuilder) => new OrgBuilder(this),
_ => throw new InvalidOperationException($"Factory does not know how to instantiate: {typeof(TBuilder).Name}")
};

Expand Down
7 changes: 7 additions & 0 deletions tests/Examples/Relations/Country.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Queo.Commons.Builders.Model.Examples.Relations;

public class Country(string name)
{
public string Name => name;
public President? President { set; get; }
}
40 changes: 40 additions & 0 deletions tests/Examples/Relations/CountryBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

using System;

using Queo.Commons.Builders.Model.Builder;
using Queo.Commons.Builders.Model.Factory;

namespace Queo.Commons.Builders.Model.Examples.Relations;

public class CountryBuilder : ModelBuilder<Country>
{
private string _name;
private IBuilder<President>? _president;

public CountryBuilder(IBuilderFactory factory) : base(factory)
{
_name = $"Country-{BuilderIndex}";
_president = null;
}

public CountryBuilder WithName(string name) => Set(() => _name = name);
public CountryBuilder WithPresident(Action<PresidentBuilder> buildAction) => Set(() =>
{
var builder = FromAction<PresidentBuilder, President>(buildAction);
builder.WithCountry(this);
_president = builder;
});

protected override Country BuildModel() => new(_name);

protected override void AfterModel(Country model)
{
if (_president is not null)
{
// President can only be build, if the country is already available
model.President = _president.Build();
}
}

protected override CountryBuilder Set(Action action) => Set<CountryBuilder>(action);
}
12 changes: 12 additions & 0 deletions tests/Examples/Relations/Org.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;

namespace Queo.Commons.Builders.Model.Examples.Relations;

public class Org(string name, User admin)
{
public string Name => name;
public User Admin => admin;

public ICollection<User> Members { get; } = [];
}

55 changes: 55 additions & 0 deletions tests/Examples/Relations/OrgBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;

using Queo.Commons.Builders.Model.Builder;
using Queo.Commons.Builders.Model.Factory;

namespace Queo.Commons.Builders.Model.Examples.Relations;

public class OrgBuilder : ModelBuilder<Org>
{
public string _name;
public IBuilder<User> _admin;
public BuilderCollection<UserBuilder, User> _members;

public OrgBuilder(IBuilderFactory factory) : base(factory)
{
_name = $"Org-{BuilderIndex}";
_admin = factory.Create<UserBuilder>();

_members = new(factory);
}

public OrgBuilder WithName(string name) => Set(() => _name = name);

public OrgBuilder WithAdmin(IBuilder<User> admin) => Set(() => _admin = admin);
public OrgBuilder WithAdmin(Action<UserBuilder> buildAction) => Set(() =>
{
_admin = FromAction<UserBuilder, User>(buildAction);
});

public OrgBuilder AddMember(IBuilder<User> member) => Set(() => _members.Add(member));
public OrgBuilder AddMember(Action<UserBuilder> buildAction) => Set(() =>
{
_members.Add(buildAction);
});

protected override Org BuildModel()
{
User admin = _admin.Build();
Org o = new Org(_name, _admin.Build());

admin.Org = o;
o.Members.Add(admin);

foreach (var member in _members.BuildModels())
{
member.Org = o;
o.Members.Add(member);
}

return o;
}

protected override OrgBuilder Set(Action action) => Set<OrgBuilder>(action);
}

7 changes: 7 additions & 0 deletions tests/Examples/Relations/President.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Queo.Commons.Builders.Model.Examples.Relations;

public class President(string name, Country country)
{
public string Name => name;
public Country Country => country;
}
36 changes: 36 additions & 0 deletions tests/Examples/Relations/PresidentBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

using System;

using Queo.Commons.Builders.Model.Builder;
using Queo.Commons.Builders.Model.Factory;

namespace Queo.Commons.Builders.Model.Examples.Relations;

public class PresidentBuilder : ModelBuilder<President>
{
private string _name;
private IBuilder<Country> _country;

public PresidentBuilder(IBuilderFactory factory) : base(factory)
{
_name = $"President-{BuilderIndex}";
_country = factory.Create<CountryBuilder>();
}

public PresidentBuilder WithName(string name) => Set(() => _name = name);
public PresidentBuilder WithCountry(IBuilder<Country> country) => Set(() => _country = country);
public PresidentBuilder WithCountry(Action<CountryBuilder> buildAction) => Set(() =>
{
_country = FromAction<CountryBuilder, Country>(buildAction);
});

protected override President BuildModel()
{
President p = new(_name, _country.Build());
_country.Build().President = p;

return p;
}
protected override PresidentBuilder Set(Action action) => Set<PresidentBuilder>(action);
}

7 changes: 7 additions & 0 deletions tests/Examples/Relations/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Queo.Commons.Builders.Model.Examples.Relations;

public class User(string name)
{
public string Name = name;
public Org? Org { set; get; }
}
40 changes: 40 additions & 0 deletions tests/Examples/Relations/UserBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

using System;

using Queo.Commons.Builders.Model.Builder;
using Queo.Commons.Builders.Model.Factory;

namespace Queo.Commons.Builders.Model.Examples.Relations;

public class UserBuilder : ModelBuilder<User>
{
private string _name;
private IBuilder<Org>? _org;

public UserBuilder(IBuilderFactory factory) : base(factory)
{
_name = $"User-{BuilderIndex}";
_org = null;
}

public UserBuilder WithName(string name) => Set(() => _name = name);
public UserBuilder WithOrg(IBuilder<Org> org) => Set(() => _org = org);
public UserBuilder WithOrg(Action<OrgBuilder> buildAction) => Set(() =>
{
_org = FromAction<OrgBuilder, Org>(buildAction);
});

protected override User BuildModel() => new(_name);

protected override void AfterModel(User model)
{
if (_org is not null)
{
var org = _org.Build();
model.Org = org;
org.Members.Add(model);
}
}

protected override UserBuilder Set(Action action) => Set<UserBuilder>(action);
}

0 comments on commit 41b1d4b

Please sign in to comment.