m31coding / M31.FluentAPI

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

Feature: External Outcome and Generics #9

Closed vzam closed 4 months ago

vzam commented 4 months ago

Hey, currently we are creating the builder right inside the source code of the class that should be built. This pollutes the actual class and often times we don't have full control over the class that should be built, so it would be nice if the library would support building an other object from the builder. This is currently already possible using extension methods:

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

public static class StudentBuilderExtensions {
  public static ThirdPartyStudent Build(this StudentBuilder builder) {
    return new ThirdPartyStudent(builder.Model, ...);
  }
}

but this could be made somewhat easier, here is an example how it could work:

[FluentApi]
public class StudentBuilder { // cherry on top: define this as partial and generate the methods as part of this class, because naming is hard already and having two classes with somewhat similar names confuses users
  [FluentMember(0)]
  public string Model {get;private set;}
  // ...

  [FluentBuilder]
  private ThirdPartyStudent Build() {
    return new ThirdPartyStudent(Model, ...);
  }
}

This might seem somewhat unnecessary at first, but I think it could be a bridge to a much greater feature, which is Generics. I am aware that Generics are hard to implement, because they might not map exactly to the type parameters of the built class and then there are type constraints. The approach that I am suggesting here would make it easier for you as the maintainer to support generics, but would also reveal much more possibilities to use the library:

[FluentApi]
public class StudentBuilder<TBuilderModel> { // cherry on top: define this as partial and generate the methods as part of this class, because naming is hard already
  [FluentMember(0)]
  private TBuilderModel Model {get; set;}
  // ...

  [FluentBuilder]
  private ThirdPartyStudent<TResultModel> Build() {
    return new ThirdPartyStudent<TResultModel>(Model); // .NET compiler will ensure that the types are compatible and constraints are satisfied, library user has the responsibility to fix any issues
  }
}

// usage:
ThirdPartyStudent<string> student = CreateStudentBuilder<string>.WithModel("test");
m31coding commented 4 months ago

Hi, thank you for your ideas and suggestions! I appreciate your insights. Generics are currently being implemented and are expected to be completed soon.

Regarding the semantics of the M31.FluentAPI library, I'm inclined to maintain the existing mechanism where you annotate a model class with attributes to generate the builder class. With this in mind, and with the current possibilities, here's how I propose implementing your use case:

[FluentApi("CreateThirdPartyStudent")]
public class ThirdPartyStudentModel
{
    [FluentMember(0)]
    public string Member1 { get; private set; }

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

    public ThirdPartyStudent Value()
    {
        return new ThirdPartyStudent(Member1, Member2);
    }
}
ThirdPartyStudent student = CreateThirdPartyStudent.WithMember1("member1").WithMember2("member2").Value();

Admittedly, the last step with the public Value method on the StudentModel class is a bit hacky. I like your suggestion of having a dedicated attribute for return values other than the builder instance. I think a FluentReturn control attribute would nicely complement the existing control attributes FluentContinueWith and FluentBreak.

With a new FluentReturn attribute your example could become:

[FluentApi("CreateThirdPartyStudent")]
public class ThirdPartyStudentModel
{
    [FluentMember(0)]
    public string Member1 { get; private set; }

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

    [FluentMethod(2)]
    [FluentReturn]
    private ThirdPartyStudent Value()
    {
        return new ThirdPartyStudent(Member1, Member2);
    }
}

The FluentReturn attribute would halt the builder, similar to FluentBreak. If used, the return value of the decorated method will be respected (including void). Without this attribute, the return value of a fluent method must be void, as it currently stands.

Another scenario where this could be beneficial is in implementing a hash code class without an implicit conversion to int in the final step:

int hashCode = CreateHashCode
    .Add(42).Add(3.14).AddSequence(new[] { 1, 2, 3 }).Add("Hello world").Value();

Best regards, Kevin