Fluentify 1.0.0
See the version list below for details.
dotnet add package Fluentify --version 1.0.0
NuGet\Install-Package Fluentify -Version 1.0.0
<PackageReference Include="Fluentify" Version="1.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Fluentify --version 1.0.0
#r "nuget: Fluentify, 1.0.0"
// Install Fluentify as a Cake Addin #addin nuget:?package=Fluentify&version=1.0.0 // Install Fluentify as a Cake Tool #tool nuget:?package=Fluentify&version=1.0.0
Fluentify
Fluentify is a .NET Roslyn Source Generator designed to automate the creation of Fluent APIs. This tool enables engineers to rapidly develop rich, expressive, and maintainable APIs with ease. Utilizing Fluentify allows for cleaner code, easier maintenance, and more expressive interactions within your C# .NET applications.
Installation
To install Fluentify, use the following command in your package manager console:
install-package Fluentify
Usage
Fluentify automatically creates extension methods for each property on types that have the Fluentify
attribute, supporting both class
and record
types.
Class Type Usage
Class types are supported as long as the type has an accessible default constructor.
[Fluentify]
public class Person
{
public ushort Age { get; init; }
public string[] Aliases { get; init; }
public DateOnly DateOfBirth { get; init; }
public Name Name { get; init; }
}
Record Type Usage
Record types are supported without the need for any special provisions
[Fluentify]
public record Person(ushort Age, string[] Aliases, DateOnly DateOfBirth, Name Name);
Marking the record
type as partial
will generate a default constructor, allowing for the record
to be instantiated without first initializing the properties.
[Fluentify]
public partial record Person(ushort Age, string[] Aliases, DateOnly DateOfBirth, Name Name);
// Allows for instantiation without property initialization
var person = new Person();
...
Immutability
The generated extension methods preserve immutability, providing a new instance with the specified value applied to the associated property.
var original = new Person { Age = 42 };
var @new = original.WithAge(75);
Console.WriteLine(original.Age); // Displays 42
Console.WriteLine(@new.Age); // Displays 75
Auto Instantiation
The value associated with a given property can be automatically instantiated, as long as that type associated with the property adheres to the new()
constraint. A second extension method is generated for the property, accepting a Func<T, T>
delegate as its parameter, which allows for the newly instantiated value to be configured before being applied.
_ = person.WithName(name => name
.WithGiven("Avery")
.WithFamily("Brooks"));
Collection Parameterization
Values can be appended to a list as long as the property type is T[]
, IEnumerable<T>
, IReadOnlyCollection<T>
, IReadOnlyList<T>
. Property types that derive from ICollection<T>
and adhere to the new()
constraint are also supported. Unlike with scalar properties, the generated extension method accepts a params T[]
, allowing for one or more values to be specified in a single invocation.
var original = new Person { Aliases = ["Avery Franklin"] };
var @new = original.WithAliases("Benjamin Sisko");
Console.WriteLine(original.Aliases.Length); // Displays 1
Console.WriteLine(@new.Aliases.Length); // Displays 2
Custom Descriptors
The name of the generated extension method(s) can be customized via the Descriptor
attribute.
Class Type Usage
[Fluentify]
public class Person
{
[Descriptor("Aged")]
public ushort Age { get; init; }
[Descriptor("BornOn")]
public DateOnly DateOfBirth { get; init; }
[Descriptor("Named")]
public Name Name { get; init; }
}
Record Type Usage
[Fluentify]
public record Person(
[Descriptor("Aged")] ushort Age,
[Descriptor("BornOn")] DateOnly DateOfBirth,
[Descriptor("Named")] Name Name);
This allows for greater alignment with domain semantics:
_ = person
.Aged(75)
.Named(name => name
.WithGiven("Avery")
.WithFamily("Brooks"))
.BornOn(new DateOnly(1948, 10, 2));
When no custom descriptor is specified, the extension method(s) will use the following pattern for all property types, except bool
:
With{PropertyName}
For bool
, the extension method will utilize the same name as the property.
Property Exclusion
Specific properties can be excluded from generating Fluentify extension method(s) using the Ignore
attribute:
Class Type Usage
[Fluentify]
public sealed class Person
{
[Ignore]
public ushort Age { get; init; }
public DateOnly DateOfBirth { get; init; }
public Name Name { get; init; }
}
Record Type Usage
[Fluentify]
public record Person([Ignore] ushort Age, DateOnly DateOfBirth, Name Name);
This will result in an error if you try to use the ignored property in the chain:
_ = person
.WithAge(75) // IntelliSense Error: 'Person' does not contain a definition for 'WithAge'
.WithName(name => name
.WithGiven("Avery")
.WithFamily("Brooks"))
.WithDateOfBirth(new DateOnly(1948, 10, 2));
Analyzers
Fluentify includes several analyzers to assist engineers with its usage. These are:
Rule ID | Category | Severity | Notes |
---|---|---|---|
FLTFY01 | Design | Warning | Class must have an accessible parameterless constructor to use Fluentify |
FLTFY02 | Usage | Info | Descriptor is disregarded from consideration by Fluentify |
FLTFY03 | Usage | Info | Type does not utilize Fluentify |
FLTFY04 | Naming | Warning | Descriptor must adhere to the naming conventions for Methods |
FLTFY05 | Usage | Info | Type does not utilize Fluentify |
FLTFY06 | Usage | Info | Property is already disregarded from consideration by Fluentify |
How to Apply in Practice
If you are unfamiliar with Fluent Builder pattern, please review Building Complex Objects in a Simple Way with C# by Gui Ferreira.
If we take the example of the MovieBuilder
and apply Fluentify, it may look like this:
[Fluentify]
public partial record Actor(
[Descriptor("BornIn")] int Birthday,
string FirstName,
string Surname);
[Fluentify]
public partial record Movie(
Actor[] Actors,
[Descriptor("OfGenre")] Genre Genre,
[Descriptor("ReleasedOn")] DateOnly ReleasedOn,
string Title);
In this example, we did not need to create the various With
methods, nor did we need to explicitly create the Build
method, significantly reducing the effort required by the engineer to support the highly expressive Fluent approach to building the Movie
instance, demonstrated as follows:
var actual = new Movie()
.OfGenre(Genre.SciFi)
.WithTitle("Star Trek: First Contact")
.ReleasedOn(new DateOnly(1996, 12, 13))
.WithActors(actor => actor
.WithFirstName("Patrick")
.WithSurname("Stewart")
.BornIn(1940));
Naturally, using Fluentify does not preclude engineers from adding additional methods to support building, and this will often be required if you choose to adopt the guided builder approach, or if specific validations or conversions are required before the final instance can be built. For example:
public class MyService
{
public MyService(string connectionString, TimeSpan timeout)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
ArgumentOutOfRangeException.ThrowIfLessThan(timeout.TotalSeconds, 1);
ConnectionString = connectionString;
Timeout = timeout;
}
public string ConnectionString { get; }
public TimeSpan Timeout { get; }
}
[Fluentify]
public partial record MyServiceBuilder(
[Descriptor("ConnectsTo")]string ConnectionString,
[Descriptor("Waits")] int Timeout)
{
public static MyServiceBuilder Empty => new();
public MyService Build()
{
return new MyService(ConnectionString, TimeSpan.FromSeconds(Timeout));
}
}
In this example, a new instance of MyService
can be created as follows:
MyService service = MyServiceBuilder
.Empty
.ConnectsTo("Some Connection String")
.Waits(30)
.Build();
Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues to suggest improvements or add new features.
License
This project is licensed under the MIT License - see the LICENSE.md file for details.
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
- Microsoft.CodeAnalysis.Analyzers (>= 3.3.4)
- Microsoft.CodeAnalysis.CSharp.Workspaces (>= 4.10.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.