m31coding / M31.FluentAPI

Generate fluent builders for your C# classes with ease.
MIT License
94 stars 4 forks source link

Support Fluent API for nested properties using lambda expressions #14

Closed m31coding closed 3 months ago

m31coding commented 3 months ago

Overview

To improve the usability of the Fluent API for nested properties, I propose introducing a lambda method for building nested objects. Consider the following classes:

public class Student
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string HouseNumber { get; set; }
}

We want to enable the following API usage:

Student student = CreateStudent
    .WithName("Alice")
    .WithAddress(a => a.WithStreet("Market Street").WithHouseNumber("23"));

To achieve this, I would like to introduce a new attribute FluentLambda, to be used instead of FluentMember for properties that have their own Fluent API. The Fluent API classes will look like this:

[FluentApi]
public class Student
{
    [FluentMember(0)]
    public string Name { get; set; }

    [FluentLambda(1)]
    public Address Address { get; set; }
}

[FluentApi]
public class Address
{
    [FluentMember(0)]
    public string Street { get; set; }

    [FluentMember(1)]
    public string HouseNumber { get; set; }
}

Create an object for the initial step

In order to realize the described feature, an object is required that exposes the methods of the first, initial step. Currently, this object can not be obtained, since the first step is static:

Student student = CreateStudent.WithName("Alice")....

The corresponding builder method is:

public static IWithAddress WithName(string name)
{
    CreateStudent createStudent = new CreateStudent();
    createStudent.student.Name = name;
    return createStudent;
}

I propose the implementation of a method InitialStep that returns an interface ICreateStudent:

public static ICreateStudent InitialStep()
{
    return new CreateStudent();
}

where

public interface ICreateStudent : IWithName
{
}

With these additions, the WithAddress method can be implemented with the CreateAddress.InitialStep method as follows:

public Student WithAddress(Func<CreateAddress.ICreateAddress, Address> createAddress)
{
    student.Address = createAddress(CreateAddress.InitialStep());
    return student;
}

Explicit interface implementations

For the first step the CreateStudent builder must implement two methods, a static method and a non-static method:

public static IWithAddress WithName(string name)
{
    CreateStudent createStudent = new CreateStudent();
    createStudent.student.Name = name;
    return createStudent;
}

public IWithAddress WithName(string name)
{
    student.Name = name;
    return this;
}

Unfortunately, these methods collide because they have the same signature. Luckily, this can be fixed by changing all interface implementations of the builder to explicit interface implementations. With this approach, the second method has a different signature and becomes:

IWithAddress IWithName.WithName(string name)
{
    student.Name = name;
    return this;
}

The difference between a normal interface implementation and an explicit interface implementation is that for the latter, the method can only be called through the interface. For example, if you have an object SomeObject someObject, you cannot call the explicitly implemented interface method directly on someObject. However, if the variable type corresponds to the interface, such as ISomeInterface someObject, then you can call the explicitly implemented interface method.

The change to explicit interface implementations is a non-breaking change for the Fluent API library because it exposes interfaces rather than builder class objects.