Creating a Fluent API in C#

Introduction

Fluent API is a design pattern that provides more readable and intuitive code by chaining method calls together. This type of API is commonly used in implementing the Builder design pattern. This style enhances code readability, reduces boilerplate, and makes APIs easier to use. Fluent APIs are commonly seen in libraries like LINQ and Entity Framework in C#. By employing method chaining and ensuring each method returns an instance of the object, fluent APIs provide a way to create code that reads like natural language. In this article, we’ll explore how to create a fluent API in C# using a practical example of building a game world and defining locations within it.

Benefits of Fluent API

  1. Readability: Fluent APIs enable developers to write code that closely resembles natural language, making it easier to read and understand.
  2. Chaining Methods: Methods can be chained together, leading to more concise and expressive code.
  3. Reduced Boilerplate: Fluent APIs often result in less boilerplate code, as they encapsulate complex operations within chained methods.
  4. Intuitive Usage: Using a fluent API feels intuitive and often reduces the learning curve for new users of your API.

Building a Fluent API

Step 1: Define the Base Classes

We’ll start by defining the classes that will be used to create the fluent API for building a game world and its locations.

public class GameWorld
{
    private List<Location> _locations = new List<Location>();

    public GameWorld AddLocation(string name)
    {
        var location = new Location(name);
        _locations.Add(location);
        return this;
    }

    public override string ToString()
    {
        return string.Join("\n", _locations.Select(loc => loc.ToString()));
    }
}

public class Location
{
    public string Name { get; }
    public string Description { get; private set; }
    public List<string> Items { get; } = new List<string>();

    public Location(string name)
    {
        Name = name;
    }

    public Location WithDescription(string description)
    {
        Description = description;
        return this;
    }

    public Location AddItem(string item)
    {
        Items.Add(item);
        return this;
    }

    public override string ToString()
    {
        var items = Items.Any() ? $"Items: {string.Join(", ", Items)}" : "No items";
        return $"Location: {Name}\nDescription: {Description}\n{items}";
    }
}

Step 2: Implement Method Chaining

Each method in the GameWorld and Location classes returns an instance of the respective class, allowing for method chaining. This enables calls like new GameWorld().AddLocation("Forest").WithDescription("A dense forest with towering trees").AddItem("Sword").AddItem("Shield");.

Step 3: Create an Example Usage

Here’s how you can use the GameWorld and Location classes to build a game world with locations:

class Program
{
    static void Main()
    {
        var gameWorld = new GameWorld()
                            .AddLocation("Forest")
                                .WithDescription("A dense forest with towering trees")
                                .AddItem("Sword")
                                .AddItem("Shield")
                            .AddLocation("Village")
                                .WithDescription("A small village with friendly inhabitants")
                                .AddItem("Potion")
                                .AddItem("Bread");

        Console.WriteLine(gameWorld);
    }
}

Output:

Location: Forest
Description: A dense forest with towering trees
Items: Sword, Shield
Location: Village
Description: A small village with friendly inhabitants
Items: Potion, Bread

Step 4: Enhancing the Fluent API

To make the fluent API more robust and versatile, you can add additional methods and improve the existing ones. For example, you might want to add support for connecting locations or adding non-player characters (NPCs).

public class GameWorld
{
    private List<Location> _locations = new List<Location>();

    public GameWorld AddLocation(string name)
    {
        var location = new Location(name);
        _locations.Add(location);
        return this;
    }

    public Location GetLocation(string name)
    {
        return _locations.FirstOrDefault(loc => loc.Name == name);
    }

    public override string ToString()
    {
        return string.Join("\n\n", _locations.Select(loc => loc.ToString()));
    }
}

public class Location
{
    public string Name { get; }
    public string Description { get; private set; }
    public List<string> Items { get; } = new List<string>();
    public List<NPC> NPCs { get; } = new List<NPC>();
    public List<Location> ConnectedLocations { get; } = new List<Location>();

    public Location(string name)
    {
        Name = name;
    }

    public Location WithDescription(string description)
    {
        Description = description;
        return this;
    }

    public Location AddItem(string item)
    {
        Items.Add(item);
        return this;
    }

    public Location AddNPC(string name, string role)
    {
        NPCs.Add(new NPC(name, role));
        return this;
    }

    public Location ConnectTo(Location location)
    {
        ConnectedLocations.Add(location);
        return this;
    }

    public override string ToString()
    {
        var items = Items.Any() ? $"Items: {string.Join(", ", Items)}" : "No items";
        var npcs = NPCs.Any() ? $"NPCs: {string.Join(", ", NPCs.Select(npc => npc.ToString()))}" : "No NPCs";
        var connections = ConnectedLocations.Any() ? $"Connected to: {string.Join(", ", ConnectedLocations.Select(loc => loc.Name))}" : "No connections";
        return $"Location: {Name}\nDescription: {Description}\n{items}\n{npcs}\n{connections}";
    }
}

public class NPC
{
    public string Name { get; }
    public string Role { get; }

    public NPC(string name, string role)
    {
        Name = name;
        Role = role;
    }

    public override string ToString()
    {
        return $"{Name} ({Role})";
    }
}

Step 5: Example with Extended Features

Using the enhanced GameWorld and Location classes to construct a more detailed game world:

class Program
{
    static void Main()
    {
        var gameWorld = new GameWorld()
                            .AddLocation("Forest")
                                .WithDescription("A dense forest with towering trees")
                                .AddItem("Sword")
                                .AddItem("Shield")
                                .AddNPC("Elven Ranger", "Guard")
                                .AddNPC("Forest Spirit", "Guide")
                            .AddLocation("Village")
                                .WithDescription("A small village with friendly inhabitants")
                                .AddItem("Potion")
                                .AddItem("Bread")
                                .AddNPC("Villager", "Trader")
                            .GetLocation("Forest")
                                .ConnectTo(gameWorld.GetLocation("Village"));

        Console.WriteLine(gameWorld);
    }
}

Output:

Location: Forest
Description: A dense forest with towering trees
Items: Sword, Shield
NPCs: Elven Ranger (Guard), Forest Spirit (Guide)
Connected to: Village

Location: Village
Description: A small village with friendly inhabitants
Items: Potion, Bread
NPCs: Villager (Trader)
Connected to: Forest

Conclusion

Creating a fluent API in C# can greatly enhance the readability and usability of your code. By designing methods that return the same instance of the object, you enable users to chain method calls in a natural and intuitive way. This pattern is not only beneficial for creating SQL query builders or configuring objects, but it can also be applied to various scenarios like building game worlds, defining workflows, or constructing complex configurations. The key is to design your API in a way that promotes method chaining and encapsulates complexity, providing a clean and efficient interface for users.

2 thoughts on “Creating a Fluent API in C#

    1. You do the AddLocation to gameWorld first, then apply the calls to the location (like AddNPC, WithDescription, etc).
      You could write an API that adds a location to an NPC, but that’s the opposite hierarchy, so you would need to provide more context like the game world for the location, or an already existing location.
      You need to design how you expect your API to commonly be called, then optimize that pattern as the fluent api chain.

      Like

Leave a reply to Eric Cancel reply