Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Look at Fluent APIs

0.00/5 (No votes)
14 Jan 2011 1  
A look at Fluent APIs and an example of one.

Introduction

Of late, there has been a rise in the number of people using Fluent APIs, you literally see them everywhere, but what are these Fluent APIs? Where can I get me one of those?

Here is what our friend Wikipedia has to say on the matter:

In software engineering, a Fluent interface (as first coined by Eric Evans and Martin Fowler) is a way of implementing an object oriented API in a way that aims to provide for more readable code. A Fluent interface is normally implemented by using method chaining to relay the instruction context of a subsequent call (but a Fluent interface entails more than just method chaining). Generally, the context is defined through the return value of a called method self referential, where the new context is equivalent to the last context terminated through the return of a void context.

This style is marginally beneficial in readability due to its ability to provide a more fluid feel to the code, however, can be highly detrimental to debugging, as a Fluent chain constitutes a single statement, in which debuggers may not allow setting up intermediate breakpoints for instance.

-- http://en.wikipedia.org/wiki/Fluent_interface up on date 14/01/2011

This article will discuss the different types of Fluent APIs out there, and will also show you a demo app that includes a Fluent API of my own making, and shall also discuss some of the problems that you may encounter whilst creating your own Fluent API.

I should mention that this article is a very simple one (which makes a change for me), and I do not expect many people to like it, but I thought it would help some folk, so I published it any way. So if you read it and think jeez Sacha that was crap, just think back to this paragraph where I told you it would be a dead simple article.

Trust me, the next ones (they are in progress) are not so easy, and are quite hard to digest, so maybe this small one is a good thing.

To Fluent Or Not To Fluent

One of the main reasons to use Fluent APIs is (if they are well designed) that they follow the same natural language rules as we use, and as a result are a lot easier to use. I personally find them a lot easier to read, and the overall structure seems to leap out at me a lot better when I read a Fluent API.

That said, should all APIs be Fluent? Hell no, some APIs would be a right mess (too big, too many inter-dependant ordering), and let us not forget that Fluent APIs do take a little bit more time to develop, and might not be that easy to come up with, and your existing classes/methods may just not be that well suited to creating a Fluent API unless you started out with the intention of creating one in the first place. These are considerations you must take into account.

Looking at Some Example Fluent APIs

There are literally loads of Fluent interfaces out there (as I said, they are all the rage these days). I have chosen two specific ones that are outlined below. I have picked these two to talk about the different types of Fluent APIs that you may encounter.

Discussion Point 1: Fluent NHibernate

NHinernate is a well established ORM (Object Relational Mapper) for .NET. You would conventionally have a code file, let's say C#, and you would have typically configured an NHibernate mapping for this class you wish to persist using an NHibernate mapping XML file such as:

<?xml version="1.0" encoding="utf-8" ?>  
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"  
  namespace="QuickStart" assembly="QuickStart">  
 
  <class name="Cat" table="Cat">  
    <id name="Id">  
      <generator class="identity" />  
    </id>  
 
    <property name="Name">  
      <column name="Name" length="16" not-null="true" />  
    </property>  
    <property name="Sex" />  
    <many-to-one name="Mate" />  
    <bag name="Kittens">  
      <key column="mother_id" />  
        <one-to-many class="Cat" />  
      </bag>  
  </class>  
</hibernate-mapping>

You would have to produce one of these types of XML files per .NET class you wish to persist. Some folks out there thought, hey why not come up with a nice Fluent API that does the same thing, and Fluent NHibernate was born.

And here is an example of how we might Fluent NHibernate to configure a mapping for the type of Cat:

public class CatMap : ClassMap<Cat>
{
  public CatMap()
  {
    Id(x => x.Id);
    Map(x => x.Name)
      .Length(16)
      .Not.Nullable();
    Map(x => x.Sex);
    References(x => x.Mate);
    HasMany(x => x.Kittens);
  }
}

The thing that may not be obvious here is that we are not returning any values, or starting anything here, nor does the order seem to be that important; it would appear we are free to swap the order of the Fluent API around (though it is not recommended).

The Fluent NHibernate is just configuring something, so the order may not necessarily matter. There are, however, Fluent APIs where the order of the Fluent API terms applied is important, as we might be starting something that relies on previous Fluent API terms or even returns a value.

We will examine a Fluent API that starts something next, so the order of the Fluent API terms is of paramount importance.

Discussion Point 2: NServiceBus Bus Configuration

Another example is one for NServiceBus which configures its Bus like this:

Bus = NServiceBus.Configure.With()
    .DefaultBuilder()
    .XmlSerializer()
    .RijndaelEncryptionService()
    .MsmqTransport()
        .IsTransactional(false)
        .PurgeOnStartup(true)
    .UnicastBus()
        .ImpersonateSender(false)
    .LoadMessageHandlers() // need this to load MessageHandlers
    .CreateBus()
    .Start();

Note: The NServiceBus Fluent API assumes you will always end with a Start() method being called, as it is actually starting some internal object that relies on the values of the previous Fluent API terms being set.

The demo app I have included returns a value, so can be thought of as a similar example to the NServiceBus Fluent API; the ordering is important, but I will also show you how I deal with it if the user does not supply the Fluent API terms in the correct order (even though my fix is very specific to the demo app, you should still be able to see how to apply this logic to your own Fluent APIs).

The Demo Project, And Its Fluent API

For the attached demo app, I had to think of something to write a Fluent API for. I ended up picking something dead simple which is something that any WPF developer will have done on more than one occasion. So what did I choose to look at?

Quite simple really, I have written a deliberately simple Fluent API around obtaining a dummy set of data that can be fetched in a background TPL Task, and enables grouping and sorting to be specified and returns a ICollectionView which is used in a dead simple MainWindowViewModel that the MainWindow of the demo app uses.

Like I say, I deliberately set out to make a very simple Fluent API, so people could see the concept, it could be more elegant, but I wanted to oversimplify it so people could see how to craft their own Fluent APIs.

Here is a screenshot of the attached demo code running:

So let's have a look at the MainWindowViewModel code which is shown below. The most relevant bits of this code are the three ICommand.Execute() methods and the private helper methods these use.

using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Diagnostics;
using FluentDemo.Data.Common;
using FluentDemo.Providers;
using FluentDemo.Model;
using FluentDemo.Commands;
using System.Windows.Threading;
using System.Windows.Data;


namespace FluentDemo.ViewModels
{
    public class MainWindowViewModel : INPCBase
    {
        private ICollectionView demoData;

        public MainWindowViewModel()
        {
            ViewLoadedCommand = 
                new SimpleCommand<object, object>(ExecuteViewLoadedCommand);
            
            PopulateAsyncCommand = 
                new SimpleCommand<object, object>(ExecutePopulateAsyncCommand);
            
            PopulateSyncCommand = 
                new SimpleCommand<object, object>(ExecutePopulateSyncCommand);

            InCorrectFluentAPIOrderCommand = 
                new SimpleCommand<object, object>(
                ExecuteInCorrectFluentAPIOrderCommand);
            
        }

        private void SetDemoData(ICollectionView icv)
        {
            DemoData = icv;
        }

        public SimpleCommand<object, object> ViewLoadedCommand { get; private set; }
        public SimpleCommand<object, object> PopulateAsyncCommand { get; private set; }
        public SimpleCommand<object, object> PopulateSyncCommand { get; private set; }
        public SimpleCommand<object, object> 
               InCorrectFluentAPIOrderCommand { get; private set; }

        public ICollectionView DemoData
        {
            get
            {
                return demoData;
            }
            set
            {
                if (demoData != value)
                {
                    demoData = value;
                    NotifyPropertyChanged(new PropertyChangedEventArgs("DemoData"));
                }
            }
        }

        private void ExecuteViewLoadedCommand(object args)
        {
            PopulateSync();
        }

        private void ExecutePopulateAsyncCommand(object args)
        {
            PopulateAsync();
        }

        private void ExecutePopulateSyncCommand(object args)
        {
            PopulateSync();
        }

        private void ExecuteInCorrectFluentAPIOrderCommand(object args)
        {
            //NON-Threaded Version, with incorrect Fluent API ordering
            //Oh no, so how do we apply our sorting/grouping, we have missed
            //our opportunity, as when we call Run() we get a ICollectionView
            
            DemoData = new DummyModelDataProvider()
            //this actually returns ICollectionView,
            //so we are effectively at end of Fluent API calls
            .Run() 
            //But help is at hand, with some clever
            //extension methods on ICollectionView, we can preempt
            //the user doing this, and still get things
            //to work, and make it look like a fluent API
            .SortBy(x => x.LName, ListSortDirection.Ascending)
            .GroupBy(x => x.Location);
        }


        private void PopulateAsync()
        {
            //Threaded Version, with correct Fluent API ordering
            new DummyModelDataProvider()
                .IsThreaded()
                .SortBy(x => x.LName, ListSortDirection.Descending)
                .GroupBy(x => x.Location)
                .RunThreadedWithCallback(SetDemoData);
        }

        private void PopulateSync()
        {
            //NON-Threaded Version, with correct Fluent API ordering
            DemoData = new DummyModelDataProvider()
            .SortBy(x => x.LName, ListSortDirection.Descending)
            .GroupBy(x => x.Location)
            .Run();
        }
    }
}

See the simple Fluent API in action in the private methods above. Let's take the most complicated of these three examples and talk about it a bit before we go on to take a look at this article's simple Fluent API. The PopulateAsync() method is the most complicated, which is as shown below:

//Threaded Version, with correct Fluent API ordering
new DummyModelDataProvider()
    .IsThreaded()
    .SortBy(x => x.LName, ListSortDirection.Descending)
    .GroupBy(x => x.Location)
    .RunThreadedWithCallback(SetDemoData);

So what is going on there? Well, a few things:

  1. We are stating we want the DummyModelDataProvider to be run threaded, so we could reasonably assume that it will be run in the background.
  2. We are specifying that we want the results sorted using the LName field of the DummyModelDataProviders fetched data.
  3. We are specifying that we want the results grouped using the Location field of the DummyModelDataProviders fetched data.
  4. We are also supplying a callback for the threaded operation to call back to when it completes.

Now that reads pretty well, I think.

One thing to note though is that like the NServiceBus example we saw earlier, the order is important. If the RunThreadedWithCallback(..) method is called first, it would not be possible to use the other Fluent API terms. Or if we called the RunThreadedWithCallback(..) method before we called the IsThreaded(..) method, it would fail as the code does not yet know it has to run in the background. I know the IsThreaded(..) method is effectively redundant; we could infer that the code should be threaded if we are in the RunThreadedWithCallback(..) method, but as I said, this example is dumb to illustrate the dangers/advantages of Fluent APIs, so I made it totally stupid, with this redundant IsThreaded(..) method in there for that reason.

So how about we look at this demo app's Fluent API then (as Mr. T from the A-Team would say "quit your jibber jabber fool").

Well, here is it all, this is it in its entirety:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;

namespace FluentDemo.Providers
{
    public class SearchResult<T>
    {
        readonly T package;
        readonly Exception error;

        public T Package { get { return package; } }
        public Exception Error { get { return error; } }

        public SearchResult(T package, Exception error)
        {
            this.package = package;
            this.error = error;
        }
    }

    public class DummyModelDataProvider
    {
        #region Data
        private enum SortOrGroup { Sort=1, Group};
        private bool isThreaded = false;
        private string sortDescription = string.Empty;
        private string groupDescription = string.Empty;
        private ListSortDirection sortDirection = 
                ListSortDirection.Ascending;
        #endregion

        #region Fluent interface
        public DummyModelDataProvider IsThreaded()
        {
            isThreaded = true;
            return this;
        }

        /// <summary>
        /// SortBy
        /// </summary>
        public DummyModelDataProvider SortBy(
            Expression<Func<DummyModel, Object>> sortExpression, 
            ListSortDirection sortDirection)
        {
            this.sortDescription = 
                ObjectHelper.GetPropertyName(sortExpression);
            this.sortDirection = sortDirection;
            return this;
        }

        /// <summary>
        /// GroupBy
        /// </summary>
        public DummyModelDataProvider GroupBy(
            Expression<Func<DummyModel, Object>> groupExpression)
        {
            this.groupDescription = 
                ObjectHelper.GetPropertyName(groupExpression);
            return this;
        }


        public ICollectionView Run()
        {
            ICollectionView collectionView = 
                CollectionViewSource.GetDefaultView(GetItems(false));

            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Sort, sortDescription);
            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Group, groupDescription);

            return collectionView;
        }


        public void RunThreadedWithCallback(
            Action<ICollectionView> threadCallBack)
        {
            ICollectionView collectionView = null;

            if (threadCallBack == null)
                throw new ApplicationException("threadCallBack can not be null");
          
            GetAll(
                (data) =>
                {
                    if (data != null)
                    {
                        collectionView = CollectionViewSource.GetDefaultView(data);

                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Sort, sortDescription);
                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Group, groupDescription);
                    }
                    threadCallBack(collectionView);
                },
                (ex) =>
                {
                    throw ex;
                });
        }
        #endregion

        #region Private Methods
        private ICollectionView ApplySortOrGroup(ICollectionView icv, 
            SortOrGroup operation, string val)
        {
            if (string.IsNullOrEmpty(val))
                return icv;

            using (icv.DeferRefresh())
            {
                icv.GroupDescriptions.Clear();
                icv.SortDescriptions.Clear();

                switch (operation)
                {
                    case SortOrGroup.Sort:
                        icv.SortDescriptions.Add(
                            new SortDescription(val, sortDirection));
                        break;

                    case SortOrGroup.Group:
                        icv.GroupDescriptions.Add(
                            new PropertyGroupDescription(val, null, 
                            StringComparison.InvariantCultureIgnoreCase));
                        break;
                }
            }


            return icv;
        }


        //This is obviously just a simulated list, this would come from Web Service
        //or whatever source your data comes from
        private List<DummyModel> GetItems(bool isAsync) 
        {
            List<DummyModel> items = new List<DummyModel>();
            items.Add(new DummyModel("UK","sacha","barber", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("UK", "sacha", "distell", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Greece","sam","bard", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Brazil","sarah","burns", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "gabriel","barnett", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "gabe", "burns", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Ireland", "hale","yeds", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("New Zealand", "harlen","frets", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "ryan", "oberon", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Australia", "tim", "meadows", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
            items.Add(new DummyModel("Thailand", "dwayne", "zarconi", 
                isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));

            //add a few more if being called in async mode
            //just so user sees a change in the UI
            if (isAsync)
            {
                items.Add(new DummyModel("Australia", "elvis", "maandrake", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
                items.Add(new DummyModel("Australia", "tony", "montana", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));
                items.Add(new DummyModel("Ireland", "esmerelda", "klakenhoffen", 
                    isThreaded ? ThreadingModel.Threaded : ThreadingModel.NotThreaded));

            }

            return items.ToList();
        }

        private void GetAll(
            Action<IEnumerable<DummyModel>> resultCallback, 
            Action<Exception> errorCallback)
        {
            Task<SearchResult<IEnumerable<DummyModel>>> task =
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        return new SearchResult<IEnumerable<DummyModel>>(
                                   GetItems(true), null);
                    }
                    catch (Exception ex)
                    {
                        return new SearchResult<IEnumerable<DummyModel>>(null, ex);
                    }
                });

            task.ContinueWith(r =>
            {
                if (r.Result.Error != null)
                {
                    errorCallback(r.Result.Error);
                }
                else
                {
                    resultCallback(r.Result.Package);
                }
            }, CancellationToken.None, TaskContinuationOptions.None,
                TaskScheduler.FromCurrentSynchronizationContext());
        }
        #endregion
    }
}

Which may look bad, but if we just show the Fluent API, look what we get:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using FluentDemo.Data.Common;
using System.Linq.Expressions;
using FluentDemo.Model;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;

namespace FluentDemo.Providers
{
    public class DummyModelDataProvider
    {
        #region Data
        private enum SortOrGroup { Sort=1, Group};
        private bool isThreaded = false;
        private string sortDescription = string.Empty;
        private string groupDescription = string.Empty;
        private ListSortDirection sortDirection = ListSortDirection.Ascending;
        #endregion

        #region Fluent interface
        public DummyModelDataProvider IsThreaded()
        {
            isThreaded = true;
            return this;
        }

        /// <summary>
        /// SortBy
        /// </summary>
        public DummyModelDataProvider SortBy(
            Expression<Func<DummyModel, Object>> sortExpression, 
            ListSortDirection sortDirection)
        {
            this.sortDescription = 
                ObjectHelper.GetPropertyName(sortExpression);
            this.sortDirection = sortDirection;
            return this;
        }


        /// <summary>
        /// GroupBy
        /// </summary>
        public DummyModelDataProvider GroupBy(
            Expression<Func<DummyModel, Object>> groupExpression)
        {
            this.groupDescription = 
                ObjectHelper.GetPropertyName(groupExpression);
            return this;
        }


        public ICollectionView Run()
        {
            ICollectionView collectionView = 
                CollectionViewSource.GetDefaultView(GetItems(false));

            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Sort, sortDescription);
            collectionView = ApplySortOrGroup(
                collectionView, SortOrGroup.Group, groupDescription);

            return collectionView;
        }

        public void RunThreadedWithCallback(
            Action<ICollectionView> threadCallBack)
        {
            ICollectionView collectionView = null;

            if (threadCallBack == null)
                throw new ApplicationException("threadCallBack can not be null");
          
            GetAll(
                (data) =>
                {
                    if (data != null)
                    {
                        collectionView = CollectionViewSource.GetDefaultView(data);

                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Sort, sortDescription);
                        collectionView = ApplySortOrGroup(
                            collectionView, SortOrGroup.Group, groupDescription);
                    }
                    threadCallBack(collectionView);
                },
                (ex) =>
                {
                    throw ex;
                });
        }
        #endregion

    }
}

See how easy that has become in the following methods? All we actually do is set an internal field to represent the action of calling the Fluent API method, and return ourselves (this):

  • IsThreaded()
  • SortBy()
  • GroupBy()

The last part of the Fluent API are the Run() or RunThreadedWithCallback() methods; these are expected to be the final methods called. Now, there is nothing to stop the user calling things in any order they want, so they could completely bypass the:

  • IsThreaded()
  • SortBy()
  • GroupBy()

method calls entirely, and just call the Run() or RunThreadedWithCallback() which returns an ICollectionView and there is nothing we can do to stop that. We can, however, use another .NET trick, which is Extension Methods, so we can make it look like a Fluent API, even after they have bypassed the normal Fluent API ordering.

There is an example of this in the demo app, where I deliberately do not follow the demo app's Fluent API and call the Run() method too early, which returns an UnSorted/UnGrouped ICollectionView. Here is that code:

DemoData = new DummyModelDataProvider()
//this actually returns ICollectionView,
//so we are effectively at end of Fluent API calls
.Run() 
//But help is at hand, with some clever
//extension methods on ICollectionView, we can preempt
//the user doing this, and still get things to work,
//and make it look like a fluent API
.SortBy(x => x.LName, ListSortDirection.Ascending)
.GroupBy(x => x.Location);

But the demo app also provides some extension methods to ICollectionView which kind of preempt someone doing this, and as we can see from the snippet above, the Fluent API'ness is still preserved.

Here are the relevant ICollectionView Extension Methods. It's a cheap parlour trick, but it works in this case, and is something you could use in your own Fluent APIs:

public static class ProviderExtensions
{
    public static ICollectionView SortBy(this ICollectionView icv, 
           Expression<Func<DummyModel, Object>> sortExpression, 
           ListSortDirection sortDirection)
    {
        icv.SortDescriptions.Add(new SortDescription(
            ObjectHelper.GetPropertyName(sortExpression), sortDirection));
        return icv;
    }

    public static ICollectionView GroupBy(this ICollectionView icv, 
           Expression<Func<DummyModel, Object>> groupExpression)
    {
        icv.GroupDescriptions.Add(
            new PropertyGroupDescription(
                ObjectHelper.GetPropertyName(groupExpression)));
        return icv;
    }
}

That's It

Anyway, that is it for now. I know it is not my normal style article, but I do like to write stuff like this too, so if you feel like voting/commenting, please go ahead, gratefully received.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here