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 different type 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 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 Flent 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 2 specific ones, that are outlined below. I have picked this 2 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 you have a code file, l;ets say C#, and you would have typically configured a NHibernate mapping for this class you wish to persist using a 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 type 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 an 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 nessecarily matter. There are however Fluent APIs where the order of the Fluent APIs 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 order of 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 : That the NServiceBus fluent API assumes you WILL ALWAYS end with a Start() method being called, as it is actually starting some internal obect that relies on 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 similiar 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 end up picking something dead simple which is something that any WPF developer will have done on more than 1 occassion. 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 screen shot of the attached demo code running:

So lets have a look at the MainWindowViewModel code which is shown below. The most relevant bits of this code are the 3 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 that simple Fluent API in action in the private methods above. Let's take the most complicated of these 3 examples and taalk about it a bit before we go on to take a look at this articles 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 reasonable 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 is that the order is important. If the RunThreadedWithCallback(..) method is called 1st, 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 apps Fluent API then (as Mr T from the A-Team would say "Quit your jibber jabber fool")

Well here is all is, 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 that 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 call entirely, and just call the have Run() or RunThreadedWithCallback() which return 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 bypassed the normal Fluent API ordering.

There is an example of this in the demo app, where I deliberately do not follow the demo apps Fluent API and call the Run() method to early which returns a 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. Its 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.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架