m31coding / M31.FluentAPI

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

Extension methods outside builder for optional properties #28

Open Sam13 opened 1 month ago

Sam13 commented 1 month ago

What about generating additional extension methods for optional properties which can be populated in any order after fluent builder was fully executed?

Example:

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

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

    ...
    public string? FavoriteFood { get; private set; }
    public string? Hobby { get; private set; }
 }

Usage:

Student alice1 = CreateStudent
  .WithFirstName("Alice").
  .WithLastName("TestName")
  .WithHobby("Reading")
  .WithFavoriteFood("Pizza");

Student alice2 = CreateStudent
  .WithFirstName("Alice").
  .WithLastName("TestName")
  .WithFavoriteFood("Pizza")
  .WithHobby("Reading");

Generated code (pseudo code):

public static Student WithHobby(this Student student, string hobby)
{
  student.Hobby = hobby; // Probably set via reflection
  return student;
}
m31coding commented 1 month ago

Hi @Sam13,

Thank you for this!

What you want to achieve is already possible like so:

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

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

    [FluentMember(2)]
    [FluentContinueWith(2)]
    public string? FavoriteFood { get; private set; }

    [FluentMember(2)]
    [FluentContinueWith(2)]
    public string? Hobby { get; private set; }

    [FluentMethod(2)]
    public void Create()
    {
    }
}

A final method, called Create in this example, is needed in the last step to conclude the setting of the optional properties:

Student alice1 = CreateStudent
  .WithFirstName("Alice")
  .WithLastName("TestName")
  .WithHobby("Reading")
  .WithFavoriteFood("Pizza")
  .Create();

Admittedly, the attributes that have to be specified for this group of optional properties are not very intuitive. Maybe we can come up with a better syntax for this.

Happy coding! Kevin

Sam13 commented 1 month ago

Hi @m31coding

Those methods are still part of the builder, aren't they? What I would prefer are ordinary extension methods on the original type. This does not need a dummy final method.

As example the method of my first post

public static Student WithHobby(this Student student, string hobby)
{
  student.Hobby = hobby; // Probably set via reflection
  return student;
}

What do you think?

m31coding commented 1 month ago

Ah, now I understand your approach. As you pointed out, no dummy final method would be needed. However, the downside is that the extension method remains available after the instance is constructed, which undermines the immutability of the Student class.

I lean towards bundling the methods for building the instance within the builder class. I agree that the final dummy method is not ideal. Another way to avoid it could be using an implicit conversion operator. For this to work, the fluent API would need to expose a class instance (rather than an interface) in the final step, allowing for the setting of optional properties and the implicit conversion to a Student instance.

Sam13 commented 1 month ago

Ah, now I understand your approach. As you pointed out, no dummy final method would be needed. However, the downside is that the extension method remains available after the instance is constructed, which undermines the immutability of the Student class.

That's exactly what I want - the extension methods should be available after construction. This allows creating an instance which can be customized later on - even in code outside of my control... With the builder you create the instance with the mandatory properties and with the extension you can customize the optional properties.

If you want immutable data structures you may use C# records?

Assuming Student is a record the extension methods would look like that:

public static Student WithHobby(this Student student, string hobby)
{
  return student with { Hobby = hobby };
}