A Fluent API Design Pattern (Part 1)

Download source code for this sample

I’ve been a fan of Fluent APIs for a considerably long time now, and given their rise in popularity over the past few years I’m clearly not alone.  There are several approaches one can take to creating their own fluent API, each with their own pitfalls and merits.  In this 2 part series, we’ll take a look at an approach that I’ve been using that tries to meet these goals:

  • Fluent API design should allow for deferred execution of chained methods.
  • Fluent APIs should be easy to develop and basically should surface and expose features of other classes, with their own set of responsibilities.
  • Fluent APIs should be easy to consume, exposing the right amount of functionality to an end-developer at the right time.

Re-using a paradigm that’s tried and true, this is my API design for a simple (and little-featured) fluent scheduling API.  This is really a model I’ve created for demonstration only; if you’re looking for a more fully featured API, you may want to check out the many options on CodePlex.

The output of this API is syntax like this:

// Starting a job 10 minutes from now.
var schedule = Schedule.Create()
	.StartAt(10.Minutes().From(now))
	.ToSchedule();
// Repeat a job infinately every 10 seconds and retrieve the next instance.
var schedule = Schedule.Create()
	.StartAt(now)
	.Repeat()
	.Every(10.Seconds())
	.Next()
	.ToSchedule();
// Conditional scheduling, even when contradicting.
var schedule = Schedule.Create()   		// Create a schedule
	.StartAt(now)						// Start it now
	.Repeat()							// Repeat it
	.Every(2.Seconds())					// Every 2 seconds
	.While(s => s.OccurrenceNumber < 3) // Up to 2 occurrences
	.Take(3)							// But take the first 3 occurrences :)
	.ToSchedule();

In order to get these results, there are really 3 pieces at work.

  1. Syntax<T> – A monad that can contain any other type.  It can easily be converted to/from it’s contained type, and provides the starting point for this example.
  2. Extension methods that extend Syntax<T>.
  3. T, in this case a Schedule type, which contains the responsibilities for maintaining underlying scheduling information.

I first start with Syntax<T>, a simple class which contains another value, of type T.  It began like this:

public class Syntax<T> : ISyntax<T>
{
	#region Constructor(s)

	public Syntax()
	{
		this.Value = default(T);
	}

	public Syntax(T value)
	{
		this.Value = value;
	}

	#endregion

	#region Properties

	public T Value { get; set; }

	#endregion
}

We’re going to use this class to contain another object, exposed as .Value.  To keep things fluent, we want to avoid excess casting (or at least it’s visibility), so things like type conversion and equality should be accounted for.  In other words, Syntax<T> should be passed into methods or injected as parameters as if it were an object of T.  So, we add a few operator overloads and an Equals() method.

#region Equality Members

public override bool Equals(object obj)
{
	if (!(obj is Syntax<T> || obj is T))
		return false;

	return Equals((Syntax<T>)obj);
}

protected virtual bool Equals(Syntax<T> other)
{
	return Equals(this.Value, other.Value);
}

public override int GetHashCode()
{
	return this.Value.GetHashCode();
}

#endregion

#region Operator Overloading

public static bool operator ==(Syntax<T> left, T right)
{
	return left != null && (object)left.Value == (object)right;
}

public static bool operator ==(T left, Syntax<T> right)
{
	return right != null && (object)left == (object)right.Value;
}

public static bool operator !=(Syntax<T> left, T right)
{
	return left != null && (object)left.Value != (object)right;
}

public static bool operator !=(T left, Syntax<T> right)
{
	return right != null && (object)left != (object)right.Value;
}

public static implicit operator Syntax<T>(T value)
{
	return new Syntax<T>(value);
}

public static explicit operator T(Syntax<T> item)
{
	return item == null ? default(T) : item.Value;
}

#endregion

The resulting class can be extended a number of ways (including extension methods) with fluency added without affecting the contained object’s properties and methods.  Here are a few unit tests that demonstrate how this works:

[TestMethod]
[Owner("Brandon Kelly")]
[Description("Tests equality operations on Syntax<T> wrapping value types.")]
public void TestEqualityForValueTypes()
{
	var valueSyntax = new Syntax<DateTime>();

	// Both the value and the syntax itself should be equal to the default value type.
	Assert.AreEqual(default(DateTime), valueSyntax);
	Assert.AreEqual(default(DateTime), valueSyntax.Value);

	// Null doesn't apply to value types, so these should also be true.
	Assert.IsNotNull(valueSyntax);
	Assert.IsNotNull(valueSyntax.Value);

	// Assignment should work through implicit conversion.
	var now = DateTime.Now;
	valueSyntax = now;

	Assert.AreEqual(now, valueSyntax);
}

[TestMethod]
[Owner("Brandon Kelly")]
[Description("Tests equality operations on Syntax<T> wrapping reference types.")]
public void TestEqualityForReferenceTypes()
{
	var referenceSyntax = new Syntax<object>();

	// The syntax itself isn't null.. at least when inspected using an assertion like this one.
	Assert.IsNotNull(referenceSyntax);

	// The value is.
	Assert.IsNull(referenceSyntax.Value);

	// Oh, and thanks to operator overloading, this also will equate to null.
	Assert.IsTrue(referenceSyntax == null);
}

[TestMethod]
[Owner("Brandon Kelly")]
[Description("Tests equality operations on Syntax<T> wrapping immutable reference types.")]
public void TestEqualityForImmutableTypes()
{
	var @object = new { IntProperty = 0 };
	var referenceSyntax = new Syntax<object>(@object);

	Assert.IsNotNull(referenceSyntax);
	Assert.AreEqual(@object, referenceSyntax);
}

These tests validate the following:

  • Objects that are contained in the Syntax<T> class can be treated as the contained class in the case of assignment.
  • Objects that are contained in the Syntax<T> class can be converted to T without issue.
  • Objects that are contained in the Syntax<T> class can be treated as the contained class in the case of an evaluation (==, !=, .Equals())

Some things to note:

  • You cannot overload the null coalesce operator.
  • The casting to (object) in the operator overloads is needed to prevent an infinite loop.

In part 2 of this series, we will look at how to build the Schedule class and extend its’ syntax using extension methods that extend Syntax<Schedule>.

Pingbacks and trackbacks (1)+

Comments are closed

About the author

I'm your host, Brandon Kelly.  I'd like to thank you for taking the time to read some of my thoughts on software develoment, the technology industry and the Florida developer community.

Month List

On My Blog

Recent Comments

Comment RSS