How to Design and Implement the Fluent Interface Pattern in C# | ASSIST Software Romania
get in touch
>

LIKE

SHARE

Facebook Share Tweet LinkedIn Share

FOLLOW

LinkedIn Follow Xing Follow

What is a Fluent Interface?

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

 How to Design and Implement the Fluent Interface Pattern in C# - Dimitrie Tataru - ASSIST Software - cover photo

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!

Möchten Sie mit uns in Kontakt treten?

Wenn Sie an unseren Softwareentwicklungsdienstleistungen interessiert sind, sich unserem Team anschließen möchten oder einfach mehr über uns erfahren möchten, würden wir uns freuen, von Ihnen zu hören! Schreiben Sie uns ein paar Zeilen und ein Mitglied des ASSIST-Teams wird sich so schnell wie möglich bei Ihnen melden. Wir sind sicher, dass wir Ihnen helfen können.

SETZEN SIE SICH MIT UNS IN VEBINDUNG!