M31.FluentApi
1.3.0
See the version list below for details.
dotnet add package M31.FluentApi --version 1.3.0
NuGet\Install-Package M31.FluentApi -Version 1.3.0
<PackageReference Include="M31.FluentApi" Version="1.3.0" />
paket add M31.FluentApi --version 1.3.0
#r "nuget: M31.FluentApi, 1.3.0"
// Install M31.FluentApi as a Cake Addin #addin nuget:?package=M31.FluentApi&version=1.3.0 // Install M31.FluentApi as a Cake Tool #tool nuget:?package=M31.FluentApi&version=1.3.0
Fluent APIs in C#
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.
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; }
}
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!
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- M31.FluentApi.Generator (>= 1.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.