An ASP.NET Core URL Builder by Mark Seemann
A use case for the Immutable Fluent Builder design pattern variant.
The Fluent Builder design pattern is popular in object-oriented programming. Most programmers use the mutable variant, while I favour the immutable alternative. The advantages of Immutable Fluent Builders, however, may not be immediately clear.
It inspires me when I encounter a differing perspective. Could I be wrong? Or did I fail to produce a compelling example?"I never thought of someone reusing a configured builder (soulds like too big class/SRP violation)."
It's possible that I'm wrong, but in my my recent article on Builder isomorphisms I focused on the pattern variations themselves, to the point where a convincing example wasn't my top priority.
I recently encountered a good use case for an Immutable Fluent Builder.
The code shown here is part of the sample code base that accompanies my book Code That Fits in Your Head.
Build links #
I was developing a REST API and wanted to generate some links like these:
{ "links": [ { "rel": "urn:reservations", "href": "http://localhost:53568/reservations" }, { "rel": "urn:year", "href": "http://localhost:53568/calendar/2020" }, { "rel": "urn:month", "href": "http://localhost:53568/calendar/2020/7" }, { "rel": "urn:day", "href": "http://localhost:53568/calendar/2020/7/7" } ] }
As I recently described, the ASP.NET Core Action API is tricky, and since there was some repetition, I was looking for a way to reduce the code duplication. At first I just thought I'd make a few private helper methods, but then it occurred to me that an Immutable Fluent Builder as an Adapter to the Action API might offer a fertile alternative.
UrlBuilder class #
The various Action
overloads all accept null arguments, so there's effectively no clear invariants to enforce on that dimension. While I wanted an Immutable Fluent Builder, I made all the fields nullable.
public sealed class UrlBuilder { private readonly string? action; private readonly string? controller; private readonly object? values; public UrlBuilder() { } private UrlBuilder(string? action, string? controller, object? values) { this.action = action; this.controller = controller; this.values = values; } // ...
I also gave the UrlBuilder
class a public constructor and a private copy constructor. That's the standard way I implement that pattern.
Most of the modification methods are straightforward:
public UrlBuilder WithAction(string newAction) { return new UrlBuilder(newAction, controller, values); } public UrlBuilder WithValues(object newValues) { return new UrlBuilder(action, controller, newValues); }
I wanted to encapsulate the suffix-handling behaviour I recently described in the appropriate method:
public UrlBuilder WithController(string newController) { if (newController is null) throw new ArgumentNullException(nameof(newController)); const string controllerSuffix = "controller"; var index = newController.LastIndexOf( controllerSuffix, StringComparison.OrdinalIgnoreCase); if (0 <= index) newController = newController.Remove(index); return new UrlBuilder(action, newController, values); }
The WithController
method handles both the case where newController
is suffixed by "Controller"
and the case where it isn't. I also wrote unit tests to verify that the implementation works as intended.
Finally, a Builder should have a method to build the desired object:
public Uri BuildAbsolute(IUrlHelper url) { if (url is null) throw new ArgumentNullException(nameof(url)); var actionUrl = url.Action( action, controller, values, url.ActionContext.HttpContext.Request.Scheme, url.ActionContext.HttpContext.Request.Host.ToUriComponent()); return new Uri(actionUrl); }
One could imagine also defining a BuildRelative
method, but I didn't need it.
Generating links #
Each of the objects shown above are represented by a simple Data Transfer Object:
public class LinkDto { public string? Rel { get; set; } public string? Href { get; set; } }
My next step was to define an extension method on Uri
, so that I could turn a URL into a link:
internal static LinkDto Link(this Uri uri, string rel) { return new LinkDto { Rel = rel, Href = uri.ToString() }; }
With that function I could now write code like this:
private LinkDto CreateYearLink() { return new UrlBuilder() .WithAction(nameof(CalendarController.Get)) .WithController(nameof(CalendarController)) .WithValues(new { year = DateTime.Now.Year }) .BuildAbsolute(Url) .Link("urn:year"); }
It's acceptable, but verbose. This only creates the urn:year
link; to create the urn:month
and urn:day
links, I needed similar code. Only the WithValues
method calls differed. The calls to WithAction
and WithController
were identical.
Shared Immutable Builder #
Since UrlBuilder
is immutable, I can trivially define a shared instance:
private readonly static UrlBuilder calendar = new UrlBuilder() .WithAction(nameof(CalendarController.Get)) .WithController(nameof(CalendarController));
This enabled me to write more succinct methods for each of the relationship types:
internal static LinkDto LinkToYear(this IUrlHelper url, int year) { return calendar.WithValues(new { year }).BuildAbsolute(url).Link("urn:year"); } internal static LinkDto LinkToMonth(this IUrlHelper url, int year, int month) { return calendar.WithValues(new { year, month }).BuildAbsolute(url).Link("urn:month"); } internal static LinkDto LinkToDay(this IUrlHelper url, int year, int month, int day) { return calendar.WithValues(new { year, month, day }).BuildAbsolute(url).Link("urn:day"); }
This is possible exactly because UrlBuilder
is immutable. Had the Builder been mutable, such sharing would have created an aliasing bug, as I previously described. Immutability enables reuse.
Conclusion #
I got my first taste of functional programming around 2010. Since then, when I'm not programming in F# or Haskell, I've steadily worked on identifying good ways to enjoy the benefits of functional programming in C#.
Immutability is a fairly low-hanging fruit. It requires more boilerplate code, but apart from that, it's easy to make classes immutable in C#, and Visual Studio has plenty of refactorings that make it easier.
Immutability is one of those features you're unlikely to realise that you're missing. When it's not there, you work around it, but when it's there, it simplifies many tasks.
The example you've seen in this article relates to the Fluent Builder pattern. At first glance, it seems as though a mutable Fluent Builder has the same capabilities as a corresponding Immutable Fluent Builder. You can, however, build upon shared Immutable Fluent Builders, which you can't with mutable Fluent Builders.