Coder Social home page Coder Social logo

m31.fluentapi's Introduction

Fluent APIs in C#

M31.FluentApi logo

Everybody wants to use fluent APIs but writing them is tedious. With this library providing fluent APIs for your classes becomes a breeze. Simply annotate them with attributes and the source code for the fluent API will be generated. The fluent API library leverages incremental source code generation at development time and your IDE will offer you the corresponding code completion immediately.

The generated code follows the builder design pattern and allows you to construct objects step by step. This approach avoids big constructors and results in very readable code.

license .net version version CI m31coding

Accompanying blog post: www.m31coding.com>blog>fluent-api

Installing via NuGet

Install the latest version of the package M31.FluentApi via your IDE or use the package manager console:

PM> Install-Package M31.FluentApi

A package reference will be added to your csproj file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to M31.FluentApi. Therefore, it is recommended to use the PrivateAssets metadata tag:

<PackageReference Include="M31.FluentApi" Version="1.3.0" PrivateAssets="all"/>

If you would like to examine the generated code, you may emit it by adding the following lines to your csproj file:

<PropertyGroup>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

The code can then be found in the obj/Generated folder.

Usage

If you use this library for the first time I recommend that you read the storybook:

Here is the full example from the introduction to the basics:

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

    [FluentMember(0, "Named", 1)]
    public string LastName { get; private set; }

    [FluentMember(1, "OfAge")]
    public int Age { get; private set; }

    [FluentMethod(1)]
    private void BornOn(DateOnly dateOfBirth)
    {
        DateOnly today = DateOnly.FromDateTime(DateTime.Today);
        int age = today.Year - dateOfBirth.Year;
        if (dateOfBirth > today.AddYears(-age)) age--;
        Age = age;
    }

    [FluentMember(2, "InSemester")]
    [FluentDefault("WhoStartsUniversity")]
    public int Semester { get; private set; } = 0;

    [FluentMember(3, "LivingIn")]
    [FluentDefault("LivingInBoston")]
    [FluentNullable("InUnknownCity")]
    public string? City { get; private set; } = "Boston";

    [FluentPredicate(4, "WhoIsHappy", "WhoIsSad")]
    [FluentNullable("WithUnknownMood")]
    public bool? IsHappy { get; private set; }

    [FluentCollection(5, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
    public IReadOnlyCollection<string> Friends { get; private set; }
 }

fluent-api-usage

You may have a look at the generated code for this example: CreateStudent.g.cs. Note that if you use private members or properties with a private set accessor, as it is the case in this example, the generated code will use reflection to set the properties.

Attributes

The attributes FluentApi and FluentMember are all you need in order to get started.

The attributes FluentPredicate and FluentCollection can be used instead of a FluentMember attribute if the decorated member is a boolean or a collection, respectively.

FluentDefault and FluentNullable can be used in combination with these attributes to set a default value or null, respectively.

The FluentMethod attribute is used for custom builder method implementations.

The control attribute FluentContinueWith indicates a jump to the specified builder step, and FluentBreak stops the builder. FluentReturn allows returning arbitrary types and values within the generated API.

FluentApi

FluentApi(string builderClassName = "Create{Name}")

Use this attribute for your class / struct / record. The optional parameter allows you to specify the name of the builder class that will be generated. Within the argument the template {Name} can be used, which will be replaced by the name of your decorated type.

[FluentApi]
public class Student
Student alice = CreateStudent...

FluentMember

FluentMember(int builderStep, string method = "With{Name}", int parameterPosition = 0)

Use this attribute for fields and properties of your class. They can be private but properties must have a set accessor. The builderStep parameter specifies the step in which the member can be set. With the method parameter you can specify the name of the builder method.

[FluentMember(0)]
public string FirstName { get; private set; }
...WithFirstName("Alice")...

If two FluentMember attributes with the same builder step and equal method names are specified, a compound method will be created, which is a builder method that sets multiple properties at once. For compounds the position of the parameters can be controlled by the parameter parameterPosition.

[FluentMember(0, "Named", 0)]
public string FirstName { get; private set; }

[FluentMember(0, "Named", 1)]
public string LastName { get; private set; }
...Named("Alice", "King")...

FluentPredicate

FluentPredicate(int builderStep, string method = "{Name}", string negatedMethod = "Not{Name}")

Can be used instead of a FluentMember attribute if the decorated member is of type bool. This attribute generates three methods, one for setting the value of the member to true, one for setting it to false, and one for passing the boolean value.

[FluentPredicate(4, "WhoIsHappy", "WhoIsSad")]
public bool IsHappy { get; private set; }
...WhoIsHappy()...
...WhoIsSad()...
...WhoIsHappy(true)...

FluentCollection

FluentCollection(
    int builderStep,
    string singularName,
    string withItems = "With{Name}",
    string withItem = "With{SingularName}",
    string withZeroItems = "WithZero{Name}")

Can be used instead of a FluentMember attribute if the decorated member is a collection. This attribute generates methods for setting multiple items, one item and zero items. The supported collection types can be seen in the source file CollectionInference.cs.

[FluentCollection(5, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
public IReadOnlyCollection<string> Friends { get; private set; }
....WhoseFriendsAre(new string[] { "Bob", "Carol", "Eve" })...
...WhoseFriendsAre("Bob", "Carol", "Eve")...
...WhoseFriendIs("Alice")...
...WhoHasNoFriends()...

FluentDefault

FluentDefault(string method = "WithDefault{Name}")

Can be used for fields and properties in addition to other attributes. When the generated builder method is called the member will keep its initial value.

[FluentMember(3, "LivingIn")]
[FluentDefault("LivingInBoston")]
[FluentNullable("InUnknownCity")]
public string? City { get; private set; } = "Boston";
...LivingInBoston()... // City is not changed

FluentNullable

FluentNullable(string method = "Without{Name}")

Can be used for fields and properties in addition to other attributes. Generates a builder method that sets the member to null.

[FluentMember(3, "LivingIn")]
[FluentDefault("LivingInBoston")]
[FluentNullable("InUnknownCity")]
public string? City { get; private set; } = "Boston";
...InUnknownCity()... // City is set to null

FluentMethod

FluentMethod(int builderStep, string method = "{Name}")

Use this attribute on methods to provide a custom implementation for setting values or triggering additional behavior. The decorated method must return void.

[FluentMethod(1)]
private void BornOn(DateOnly dateOfBirth)
{
    DateOnly today = DateOnly.FromDateTime(DateTime.Today);
    int age = today.Year - dateOfBirth.Year;
    if (dateOfBirth > today.AddYears(-age)) age--;
    Age = age;
}
...BornOn(new DateOnly(2003, 6, 24))...

FluentContinueWith

FluentContinueWith(int builderStep)

Can be used at all steps on fields, properties, and methods to jump to a specific builder step. Useful for skipping steps and branching. May be used to create optional builder methods:

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

[FluentMember(1)]
[FluentContinueWith(1)]
public string? MiddleName { get; private set; }

[FluentMember(1)]
public string LastName { get; private set; }
...WithFirstName("Bob").WithLastName("Bishop")...
...WithFirstName("Alice").WithMiddleName("Sophia").WithLastName("King")...

FluentBreak

FluentBreak()

Can be used at all steps on fields, properties, and methods to stop the builder. Only relevant for non-linear APIs that make use of FluentContinueWith.

[FluentMethod(1)]
[FluentBreak]
private void WhoseAddressIsUnknown()
{
}
...WhoseAddressIsUnknown();

FluentReturn

Allows the builder to respect the return value of the decorated method, enabling the return of arbitrary types and values within the generated API. If a void method is decorated with this attribute, the builder method will also return void.

[FluentMethod(1)]
[FluentReturn]
public string ToJson()
{
    return JsonSerializer.Serialize(this);
}
string serialized = ...ToJson();

Forks

To create forks specify builder methods at the same builder step. The resulting API offers all specified methods at this step but only one can be called:

[FluentMember(1, "OfAge")]
public int Age { get; private set; }

[FluentMethod(1)]
private void BornOn(DateOnly dateOfBirth)
{
    DateOnly today = DateOnly.FromDateTime(DateTime.Today);
    int age = today.Year - dateOfBirth.Year;
    if (dateOfBirth > today.AddYears(-age)) age--;
    Age = age;
}
...OfAge(22)...
...BornOn(new DateOnly(2002, 8, 3))...

Problems with the IDE

As of 2023 code generation with Roslyn is still a relatively new feature but is already supported quite well in Visual Studio and Rider. Since code generation is potentially triggered with every single key stroke, there are sometimes situations where the code completion index of the IDE does not keep up with all the changes.

In particular, if your IDE visually indicates that there are errors in your code but it compiles and runs just fine, try the following things:

  • Rebuild the project or the whole solution
  • Unload and reload the project
  • Close and reopen the IDE
  • Remove the .vs folder (Visual Studio) or the .idea folder (Rider)

Support and Contribution

This library is free. If you find it valuable and wish to express your support, please leave a star. You are kindly invited to contribute. If you see the possibility for enhancement, please create a GitHub issue and you will receive timely feedback.

Happy coding!

m31.fluentapi's People

Contributors

m31coding avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

m31.fluentapi's Issues

Feature: External Outcome and Generics

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");

FluentCollection on a nullable array results in duplicate method signatures

This fluent API definition

[FluentApi]
public class Student
{
    [FluentCollection(0, "Friend", "WhoseFriendsAre", "WhoseFriendIs", "WhoHasNoFriends")]
    public string[]? Friends { get; private set; }
}

generates several methods, including the following:

public static Student WhoseFriendsAre(string[]? friends)
{
    CreateStudent createStudent = new CreateStudent();
    friendsPropertyInfo.SetValue(createStudent.student, friends);
    return createStudent.student;
}

public static Student WhoseFriendsAre(params string[] friends)
{
    CreateStudent createStudent = new CreateStudent();
    friendsPropertyInfo.SetValue(createStudent.student, friends);
    return createStudent.student;
}

The compiler raises an error due to duplicate method signatures:

Type 'CreateStudent' already defines a member called 'WhoseFriendsAre' with the same parameter types

Feature: Allow skipping stages

I see some great use cases for the library in unit testing, to create some fake objects and convert them into my models. It would be nice if steps were skippable, such that the long lists such as:

CreateFakeStudent
  .WithRandomId()
  .WithRandomName()
  .WithRandomPhone()
  .WithBudget(100)

could be replaced with: CreateFakeStudent.WithBudget(100). This makes the intent of the faked object clearer. The WithRandomX methods I have shown are FluentDefaults so they would be implicit. A possible implementation would allow doing that:

[FluentApi]
class FakeStudent {
  [FluentMember(0)]
  [FluentSkippable]
  public Guid Id {get;set;} = Guid.NewGuid();

  [FluentMember(1)]
  [FluentSkippable]
  public string Name {get;set;} = "John Doe"; // not random but you get the point

  [FluentMember(2)]
  [FluentSkippable]
  public string Phome {get;set;} = "123";

  [FluentMember(3)]
  public int Budget {get;set;}
}

and generate:

// no interface for Id because it is the first step

interface INameSkippable : IPhoneSkippable {
  void WithName();
}

interface IPhoneSkippable : IBudget {
  void WithPhone();
}

interface IBudget {
  FakeStudent WithBudget();
}

static class CreateFakeStudent {
  static INameSkippable WithId() { ... }
  static IPhoneSkippable WithPhone() { ... }
  static IBudget WithBudget() { ... }
}

Support params attribute

Currently, when using FluentMethod with params, the params attribute is missing in the generated code. For example in public void UseThis(params string[] items), the generated code will only have string[] items as the parameter. See also ParamsArrayAttribute.

Support default values

Currently, when using FluentMethod with default values, the FluentApi does not keep the default values in the generated builder, such that the value always has to be provided. It would be nice if the generator was able to carry them over. See also ParameterInfo.DefaultValue. It would be interesting if the FluentMethod also allowed different signatures for the same method name at the same step, although I can imagine that overloads are supported already (I didn't verify that).

Allow skipping steps

As I understand it, the options to every step have to be provided and you cannot skip steps in between for a single path:

[FluentApi]
public class Api {
  [FluentMethod(0)]
  public void UseOneThing();
  
  [FluentMethod(0)]
  public void UseOtherThing();
  
  [FluentMethod(1)]
  public void ConfigureOneThing();

  [FluentMethod(1)]
  public void ConfigureOneThingTheOtherWay();
  
  [FluentMethod(2)]
  public void Continue();
}

In this example I would like to skip step 1 entirely after having selected UseOtherThing() at step 0, maybe with a new attribute [FluentSkipTo(2)] or [FluentContinueWith(2)], also it might be considered to have [FluentBreak] to finalize the object if the last steps are no longer relevant for the chosen configuration.
This can be generated as follows:

public interface IStep0
{
  IStep1 UseOneThing();
  IStep2 UseOtherThing(); // skip IStep1
}

public interface IStep1
{
  IStep2 ConfigureOneThing();
  IStep2 ConfigureOneThing();
}

public interface IStep2
{
  BuiltClass Continue();
}

I think this is a fairy common pattern and would benefit the library.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.