Practical .NET

Strategies and Code for Creating Fluent APIs

There are numerous ways to implement a fluent API, depending on the degree of control you want to maintain over the API, how many classes you want to be able to use it with, and how you want to extend your API. Here are your options.

In an earlier column, "Implementing a Fluent Interface," I showed how to create a fluent API for a single class. However, there are other strategies that offer more flexible solutions.

The original design added to a class called SalesOrder a property (named Manage) that returned a class internal to the SalesOrder class (I called that internal class ManageOrder). As shown in Listing 1, the SalesOrder class passed a reference to itself to that ManageOrder class.

Listing 1: A Fluent Interface for a Single Class
Public Class SalesOrder
  ...
  Public ReadOnly Property Manage As ManageOrder
    Get
      Return New ManageOrder(Me)
    End Get
  End Property

  Class ManageOrder
    Private Order As SalesOrder

    Public Sub New(order As SalesOrder)
      Me.Order = order
    End Sub
  ...
   End Class
...
End Class

The methods in the ManageOrder class formed the fluent API for the SalesOrder class. (I'll refer to the ManageOrder class, or its equivalent, as the "API class"). Each method in the API class can perform some operation on the SalesOrder object (the "context object") passed to the API class. The method then returns the API class to the next method in the chain. Listing 2 shows two methods from the ManageOrder class that would be used to update the collection of OrderLines on the SalesOrder.

Listing 2: Two Fluent Methods
Public Function Remove(productId As String) As ManageOrder
  Order.OrderLines.RemoveAll(Function(fi) fi.ProductId = productId)
  Return Me
End Function

Public Function Add(
           productId As String, quantity As Integer) As ManageOrder
     
  If Order.OrderLines.Where(Function(fi) fi.ProductId = productId).Count = 0 Then
    Dim ol As New OrderLine With {.ProductId = productId,
                                          .Quantity = quantity,
                                          .Order = Order}
    Order.OrderLines.Add(ol)
  End If
  Return Me
End Function

With this design, the methods making up the fluent API can be strung together to perform a series of actions on the context object. Here's how a developer using my sample API could remove one OrderLine from a SalesOrder and add a replacement OrderLine:

so.Manage.Remove("A123").
          Add("A123", 4)

Applying Inheritance and Interfaces
This is a good strategy, but as I discussed in an earlier column, "When to Build Fluent Interfaces for Re-Use and Clarity," I'd prefer a solution that allowed the methods that make up a fluent API to be applied to multiple classes. Assuming that a SalesOrder is just one of many kinds of orders that all inherit from some base class, I can achieve that goal by pushing the ManageOrder class and the Manage property into that base class:

Public Class BaseOrder
  Public ReadOnly Property Manage As ManageOrder
  ...
  End Property

  Public Class ManageOrder
  ...
  End Class
End Class

Public Class SalesOrder
    Inherits BaseOrder
  ...
End Class

While providing more flexibility, this design still limits the API to a single inheritance structure. A strategy that makes the API more generally available associates the API with an interface, rather than with a specific class or inheritance structure. This strategy makes sense when you have a number of classes with similar interfaces that don't share a common base object.

The SalesOrder API requires the class that uses it to have a collection of OrderLine (or "OrderLine-like") classes and a property that returns the ManageOrder class. Implementing the interface strategy would, therefore, require these two interfaces:

Public Interface ISalesOrder
    Property OrderLines As List(Of IOrderLine)
    ReadOnly Property Manage As ManageOrder
End Interface

Public Interface IOrderLine
    Property Quantity As Integer
    Property ProductId As String
    Property Order As SalesOrder
End Class

This strategy also requires the API class to be an external class rather than a class nested inside of the context class -- unless you seal the class that permits developers to create their own versions of your API (not necessarily a bad thing, as I'll discuss later on). Any developer that wants to use the API on their class would just need to implement the required interfaces, including providing the code for the Manage property, as shown in Listing 3.

Listing 3: Implementing the Interface Required by a Fluent API
Public Class SalesOrder
  Implements ISalesOrder
  Implements IDisposable

  Public Property OrderLines As New List(Of IOrderLine) _
                     Implements ISalesOrder.OrderLines

  Public ReadOnly Property Manage As ManageOrder _
                     Implements ISalesOrder.Manage
    Get
      Return New ManageOrder(Me)
    End Get
  End Property
   ...
End Class

Public Class OrderLine
  Implements IOrderLine

  Public Property Quantity As Integer Implements IOrderLine.Quantity
  Public Property ProductId As String Implements IOrderLine.ProductId
  Public Property Order As SalesOrder Implements IOrderLine.Order

End Class

Tying the API to an interface also ensures that the API is limited to manipulating the methods and properties defined in the interface, which helps prevent "API bloat." Without the interface, the developer responsible for the API class can extend the API to affect any member of the context class at will, potentially surprising the developers responsible for those classes. With an interface, extending the API to affect other methods and properties on the context class requires adding those methods and properties to the ISalesOrder and IOrderLine interfaces. Developers responsible for the context classes would be forced to rewrite their classes -- something those developers would resist (or, at the very least, a requirement that would make them aware of changes to the API). That requirement discourages the API developer from extending the API and, instead, to develop a second interface for a new API that supports the additional functionality. The developers responsible for the classes that use the original API could then choose whether to implement the new interface.

(Eric Vogel has a more complete discussion of the interface strategy that includes incorporating partial classes.)

Extending the API
The Open/Closed Principle suggests that every class be open to extension, though closed to modification (implementing Vogel's "You don't mess with working code" rule). While the interface strategy allows the API to be used with a variety of classes, it doesn't support creating different versions of the API itself -- something that may be necessary if the API is to support any class that implements the interface.

There are two strategies for extending the API class: Define an interface for the API class (IManageOrder) that new versions can implement, or support new versions that inherit from the existing API class. If you assume that new API versions will want to use at least some of the code in the existing API class, the second strategy is your best choice.

Implementing the inheritance strategy for the API class requires that you make the existing methods in the API class overridable, then set the scope for internal fields (like the Order field) to Protected. These changes are sufficiently trivial that it's probably worth doing even if you don't expect to create new versions of your API. You can also have your existing API class inherit from this class and not override any of the methods in the base class, though that's not necessary.

To apply this strategy to ManageOrder, I create a new base class (called FluentOrderApiBase), make the required changes and have ManageOrder inherit from it:

Public Class FluentOrderApiBase
  Protected Order As SalesOrder

  Public Sub New(order As SalesOrder)
    Me.Order = order
  End Sub

  Public Overridable Function ... Returns FluentOrderApiBase
  End Function
  ...
End Class

Public Class ManageOrder
  Inherits FluentOrderApiBase
End Class

A developer creating a new version of your API class just has to inherit from your base class and add a constructor to pass the ISalesOrder object to the base object. This strategy also allows the designer of the new API class to accept additional parameters in the new API class' constructor that can be used when overriding the base API methods:

Public Class ManageOrder
    Inherits FluentOrderApiBase
    Private ProcessingDate As Date

    Sub New(Order As ISalesOrder, ProcessingDate As Date)
        MyBase.New(Order)
        Me.ProcessingDate = ProcessingDate
    End Sub

Any class that needs to use this new version of your API will instantiate the new API class in its Manage property.

Extension Methods
You can further reduce the demands on developers who want to create classes that use your API by using an extension method. Rather than embedding a property in the context object that returns an API class object, you can create an extension method that returns your API class. If you have multiple versions of your API, you can create an extension method for each version, allowing developers to pick and choose which version of your API they want to use.

To specify where your API can be used, set the first parameter for your extension method to the class or interface the API supports. In Visual Basic, an extension method that works with any class that implements the ISalesOrder interface and returns my API class looks like this:

Public Module OrderExtensions

  <Extension()>
  Public Function Manage(Order As ISalesOrder) As ManageOrder
    Return New ManageOrder(Order)
  End Function

The syntax for creating an extension method in C# that does the same thing is different:

public static class OrderExtensions
{
  public static ManageOrder Manage(this ISalesOrder order)
  {
    return new ManageOrder(order);
  }
}

This method will now appear in the IntelliSense list for any object that implements the ISalesOrder interface. When a developer uses your method with an object, the .NET Framework will automatically pass the object into the method in the first parameter -- no explicit reference to the object is required.

Putting that all together, it means that this code still works with the Manage extension method:

so.Manage.Remove("A123").
          Add("A123", 4)

In addition, the ISalesOrder interface no longer needs the Manage property. Instead, the ISalesOrder interface becomes simpler and only requires developers to implement those members that the API methods manipulate:

Public Interface ISalesOrder
  Property OrderLines As List(Of IOrderLine)
End Interface

Eliminating the API Class
If you wish, you can eliminate the need for the API class altogether. These two extension methods, for example, can be called directly from the SalesOrder object (and each other). These methods attach themselves to any object that implements the ISalesOrder and return that object:

<Extension()>
Public Function Add(Order as ISalesOrder, ...) As ISalesOrder
  ...
  Return Order
End Function

<Extension()>
Public Function Remove(Order as ISalesOrder, ...) As ISalesOrder
  ...
  Return Order
End Function

With these extension methods in place, the code to add and remove an OrderLine is simpler because the Manage method isn't required:

so.Add("A123", 10).
   Remove("A123")

There's nothing wrong with this design. Furthermore, extending this API is easy: Just write another extension method.

However, there are benefits to using an API class. First, the API class can hold any data that might be required by any of the extension methods, reducing the parameters that need to be passed to the API methods. Using an API class also allows you to create a more sophisticated chaining mechanism. Instead of the API methods performing any activity on the context object, the methods can simply store information in the API class. At the end of the processing chain, code in the API class can validate that information and perform the required operations. The API class also provides a central point of control for implementing the fluent interface and a base for building different implementations of the API -- something not possible with extension methods.

Finally, without the API class, the object returned by the each of the API methods must be carefully thought out to ensure that developers can put together whatever chains they require. If a method in the chain returns an OrderLine object, for example, the developer is prevented from adding a subsequent method that requires access to the SalesOrder object (unless, of course, you're willing to rewrite the two objects). Because the API class can hold any information required by any method and defer processing until the end of the chain (when all the information is gathered), chains can be more flexible.

That's a lot of choices, and there are probably more strategies for implementing a fluent API than I've discussed here. The choices I've described here, though, allow you decide whether you want to implement the interface for one class, for an inheritance hierarchy, or an arbitrary set of classes that implement a common interface. The choices also allow you to exercise some control over how the API is grown and if new versions of it can be implemented. Thinking about a fluent API means not only thinking about what you need now, but what you'll want to do in the future.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

  • What's New for Python, Java in Visual Studio Code

    Microsoft announced March 2024 updates to its Python and Java extensions for Visual Studio Code, the open source-based, cross-platform code editor that has repeatedly been named the No. 1 tool in major development surveys.

Subscribe on YouTube