Read time: 6 minutes
A fluent interface is a method of designing object-oriented APIs based extensively on method chaining with the goal of making the readability of the source code to that of ordinary written prose, essentially creating a domain-specific language within the interface. — Wikipedia
I. Introduction
This article covers multiple programming concepts, including but not limited to: inheritance, polymorphism, method-chaining, DSL (Domain Specific Language), and fluent interfaces.
You may not be familiar with the term Fluent Interface API, but it is very likely that you have already used one before. It is consistently used in many frameworks and libraries across every Object-Oriented Programming language.
After reading this article, you will establish the groundwork for design requirements and implementation steps of the Fluent Interface pattern.
I. 1 Fluent Interface vs Extension Method
The Fluent Interface and the Extension Method are both difficult or sometimes even impossible to debug. Intermediate results are unattainable. The syntax is similar for both, but let's see what makes them different:
Extension Method
- The returned type is constant; the extension method returns an object of the same type.
- Looping is not prevented; the developer can chain the same method and it is executed on each call.
- Statements are executed one by one and the result is dependent on the previous operation.
Fluent Interface
- The returned type varies for each method. This assures flexibility and extensibility.
- Looping can be restrained; methods can be chained in a predetermined order.
- Statements are executed at the end. Chaining is used to collect data and establish paths.
I. 2 Subject
The prerequisite is to write a class that allows:
- Download to file/stream and upload from file/stream;
- Files transfer between the local computer and Azure Blob Storage;
- Asynchronous transfer, which is not imperative, but may be required later on.
In a standard way, implementing the following interface would satisfy the requirements:
interface IBlobTransfer { void BlobDownload(string connectionString, string blobName, string fileName, string filePath); void BlobDownload(string connectionString, string blobName, string fileName, Stream stream); void BlobUpload(string connectionString, string blobName, string fileName, string filePath); void BlobUpload(string connectionString, string blobName, string fileName, Stream stream); }
Although this would satisfy the requirements, let's go through a better way to implement them.
II. Design
II. 1 Define the natural language syntax
First of all, it is necessary to define a list with all appropriate (and allowed) method combinations.
Applying this list of combinations on the subject above results in the following:
FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToFile(..); FluentBlobTransfer.Connect(..).OnBlob(..).Download(..).ToStream(..); FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromFile(..); FluentBlobTransfer.Connect(..).OnBlob(..).Upload(..).FromStream(..);
II. 2 Restrictions
It can be seen that Download.FromFile and Upload.ToStream are some of the combinations that are not allowed.
To prevent such behavior, you must satisfy the following conditions:
- The Download method should return an interface with the following signatures:
void ToFile(..); void ToStream(..);
- The Upload method should return an interface with the following signatures:
void FromFile(..); void FromStream(..)
II. 3 Class diagram
II. 4 Define interfaces
Be creative! This is where your signature resides. Follow these simple rules for guaranteed success:
- The first and last methods are allowed to be parameterless.
- Allow no more than one argument for each method.
- Use descriptive words; you can use phrases made up of up to five words.
- Cover all cases and split paths correctly.
interface IAzureBlob { IAzureAction OnBlob(string blobBlockPath); }
interface IAzureAction { IAzureWrite Download(string fileName); IAzureRead Upload(string fileName); }
interface IAzureWrite { void ToFile(string filePath); void ToStream(Stream stream); }
interface IAzureRead { void FromFile(string filePath); void FromStream(Stream stream); }
III. Implementation
III. 1 Tips
- Constructors must be private; do not allow class instantiation.
- Do not allow inheritance; use the sealed modifier.
- Use expressive names for methods and arguments; do not use ambiguous or misleading names.
- Use at most one parameter for each method; do not forget that we are going the fluent way!
III. 1 Entry point
The entry method must be static. It is recommended for the name to be a verb in order to express the intent.
III. 2 Implement interfaces and ensure proper chaining
Each step on the chain should be used to collect data and lead to the next method.
Usually, the last chain method returns void to indicate the chain end.
When the last method returns a result object, it should be named accordingly. Use expressive words like Execute(), or GetResult() to make it clear that it is the last method.
Notice that the FluentBlobTransfer class implements all the interfaces prepared earlier.
public sealed class FluentBlobTransfer : IAzureBlob, IAzureAction, IAzureWrite, IAzureRead { private readonly string connectionString; private string blobBlockPath; private string fileName; private FluentBlobTransfer(string connectionString) => this.connectionString = connectionString; public static IAzureBlob Connect(string connectionString) => new FluentBlobTransfer(connectionString); public IAzureAction OnBlob(string blobBlockPath) { this.blobBlockPath = blobBlockPath; return this; } public IAzureWrite Download(string fileName) { this.fileName = fileName; return this; } public IAzureRead Upload(string fileName) { this.fileName = fileName; return this; } public void ToFile(string filePath) { // Code to download from Azure Blob Storage to file } public void ToStream(Stream stream) { // Code to download from Azure Blob Storage to stream } public void FromFile(string filePath) { // Code to upload from file to Azure Blob Storage } public void FromStream(Stream stream) { // Code to upload from stream to Azure Blob Storage } }
Please note that transfer implementation is beyond the scope of this article.
You can now fluently transfer files:
FluentBlobTransfer .Connect("storageAccountConnectionString") .OnBlob("blobName") .Download("fileName") .ToFile(@"D:\Azure\Downloads\");
IV. Discover
My favorite examples of Fluent APIs are Microsoft.Azure.*.Fluent libraries.
The outstanding aspect of these libraries is that they replaced the need for memorizing commands and parameters (unlike PowerShell scripts) with a self-explanatory Fluent API.
In the next snippet, a new SQL Server is created on a new Resource Group, with two databases and Firewall rules.
await Azure .Configure() .WithLogLevel(Level.Basic) .Authenticate(credentials) .WithSubscription("subscriptionId") .SqlServers .Define("sqlServerName") .WithRegion(Region.EuropeWest) .WithNewResourceGroup() .WithAdministratorLogin("sqlAdmin") .WithAdministratorPassword("pass") .WithNewDatabase("databaseOne") .WithNewDatabase("databaseTwo") .WithNewFirewallRule("0.0.0.0") .WithNewFirewallRule("1.1.1.1") .WithNewFirewallRule("2.2.2.2") .CreateAsync();
EntityFramework's Fluent API Configuration provides a way to configure database tables and columns with great syntax.
modelBuilder
.Entity<Foo>()
.HasMany(foo => foo.Bars)
.WithOne(bar => bar.Foo)
.HasForeignKey(bar => bar.FooId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder
.Entity<Foo>()
.Property(foo => foo.Value)
.HasDefaultValue("0123456789")
.HasMaxLength(10)
.IsFixedLength()
.IsRequired();
Explore other Fluent Interface samples, discover fluent-chaining, and access the source code for this subject on my GitHub account.
V. Conclusion
It can take a significant amount of time to design and implement a pleasing Fluent Interface API that is easy to interpret and use, but it is worth the effort.
You may find it overwhelming, but keep in mind that it is not necessary for everything to be fluent.
It is convenient to have a Fluent Interface for reusable code, considering that programmers can start using it without having to worry, How does it work? How many parameters do I need? How many are optional?.
Do you have a great Fluent API? Share it with us!
Thank you for reading! Happy coding!