Using the Builder Design Pattern in .NET C# to Develop a Fluent API

Written by ahmedtarekhasan | Published 2023/01/26
Tech Story Tags: dotnet | csharp | software-development | software-engineering | software-architecture | software-design | best-practices | design-patterns | web-monetization

TLDRA step-by-step guide to developing a Fluent API from scratch in.NET C# using the Builder Design Pattern. The example used in this article is just for demonstration purposes. Some best practices would be ignored in order to drive the main focus to the other best practices targeted on this article.via the TL;DR App

A step-by-step guide to developing a Fluent API from scratch in .NET C# using the Builder Design Pattern.

I am sure this is not the first time for you to hear about the Builder Design Pattern. However, I promise you that you would find something different in this article.

In this article, we would walk through the whole process of developing a Fluent API using the Builder Design Pattern, from the early steps of thinking about it, to the latest of testing it.

Therefore, buckle your seat belts, and let’s begin our trip.


What is the Builder Design Pattern?

It is a creational design pattern which allows creating complex objects into small simple steps one by one.


What are the advantages of the Builder Design Pattern?

Some of the well-known advantages of the Builder Design Pattern are:

  1. It helps breakdown the process of creating complex objects into small chunks which become more controllable.
  2. It enables using a Domain Specific Language (DSL) which the end user can relate to.
  3. It helps move from a general definition to a more specific granular definition of the object we are building.

What are the disadvantages of the Builder Design Pattern?

Mostly, it adds extra layer of complexity to the code which you would notice at the end of the implementation in this article.


How to implement the Builder Design Pattern?

Yes, this is the question I know you are interested into. However, this time we would not just jump into the code implementation. We would go through the whole process starting from the early stages of design.

Therefore, let’s start.


The Example

First, let’s come up with an example to use through or trip. I chose a simple example of a registration process of a school.

In simple words, at some point in the whole solution, you will need to define some teachers and some students. Let’s assume that these Teacher and Student objects are so complex that we need to develop a Fluent API to create them.


Disclaimer

  1. Some best practices would be ignored/dropped in order to drive the main focus to the other best practices targeted on this article.
  2. The example used in this article is just for demonstration purposes. It is not the best candidate for applying the Builder Design Pattern.
  3. We can integrate different practices with the Builder Design Pattern like using generics and other things, but, all of these are dropped to keep the example as simple as possible.
  4. There is a reasonable variance in the way of implementing the Builder Design Pattern, therefore, you might find some other different implementations than the one we re going to use in this article.
  5. Try to use the Builder Design Pattern only when actually needed as it adds complexity to the whole solution.

Peek Into The Future

If you follow the same exact steps in this article, you should end up with this solution structure:

And you would be able to write some code like this:

And this:


Sketching the Fluent API

Now, we would start with a sketch of how our Fluent API should look like. You can do this on a piece of paper, Excel sheet, or whatever sketching tool you like.

So, our sketch would be something like this:

Notes:

  1. Builder is the main entry point. From there we would move to New.
  2. Then we can have two options; WithName(name) and WithAge(age).
  3. However, on the next step, if you are already coming from WithName(name), we are only allowing WithAge(age). And following the same concept, if you are already coming from WithAge(age), we are only allowing WithName(name).
  4. Then we would be merging to one common point.
  5. From this common point, we have two options; AsTeacher and AsStudent.
  6. From AsTeacher, the flow would be Teaching(subject) >> WithSchedule(schedule).
  7. And from AsStudent, the flow would be Studying(subjects) >> WithSchedule(stydingSchedule).
  8. Finally, they all merge to the Build() command.

Defining Interfaces

Now, let’s start working on the code.

Steps

Open VS or your preferred IDE.

Create a new Class Library or Console Application. I named my project as FluentApi.

Inside my project, I created the following folders:

  1. Builder
  2. Builder\Dtos
  3. Builder\Dtos\Descriptors
  4. Builder\Implementations
  5. Builder\Interfaces

Now you need to keep an important thing in your mind, we will need to jump back and forth between Interfaces and Dtos while working on the implementation, this is normal.


Now let’s start with our first interface, IMemberBuilder. Here is an important trick. I created a file under the Interfaces folder and named it 01.IMemberBuilder.cs

This 01. at the start of the name helps me easily track the sequence of the whole process. Otherwise, for a small change, you might need to go through all the files to spot the place to apply your changes.

namespace FluentApi.Builder.Interfaces
{
    public interface IMemberBuilder
    {
        IHuman New { get; }
    }
}

We know, from the Sketch, that our Builder should expose a New property and this property should lead us to something that exposes two methods; WithName(name) and WithAge(age).

So, the New property should return, let’s say a new Interface called IHuman.


Moving to the next step, let’s define the IHuman interface. So, create a 02.IHuman.cs file, and define the interface as follows:

namespace FluentApi.Builder.Interfaces
{
    public interface IHuman
    {
        IHaveAgeAndCanHaveName WithAge(int age);
        IHaveNameAndCanHaveAge WithName(string name);
    }
}

We know, from the Sketch, that the IHuman interface should have the two methods WithName(name) and WithAge(age). However, these two methods should have different return types. Why???

Because we want that once the WithName(name) is called, the only available option is to call WithAge(age), not another WithName(name). And the same applies to WithAge(age).

Note: You might also prefer to have only one method which takes in both the name and age, this is also right but I preferred here to seize the chance to show you different options.


Moving to the next step, let’s define the IHaveAgeAndCanHaveName interface. So, create a 03.IHaveAgeAndCanHaveName.cs file, and define the interface as follows:

namespace FluentApi.Builder.Interfaces
{
    public interface IHaveAgeAndCanHaveName
    {
        IHasRole WithName(string name);
    }
}

We know, from the Sketch, that the IHaveAgeAndCanHaveName interface should have the method WithName(name). And this method should return something that exposes the AsTeacher and AsStudent properties.


Also, following the same way, let’s define the IHaveNameAndCanHaveAge interface. So, create a 03.IHaveNameAndCanHaveAge.cs file (note that the file is numbered as 03 because it is still on the third step on the whole process), and define the interface as follows:

namespace FluentApi.Builder.Interfaces
{
    public interface IHaveNameAndCanHaveAge
    {
        IHasRole WithAge(int age);
    }
}

We know, from the Sketch, that the IHaveNameAndCanHaveAge interface should have the method WithAge(age). And this method should return something that exposes the AsTeacher and AsStudent properties, the same as IHaveAgeAndCanHaveName.WithName(name).


Moving to the next step, let’s define the IHasRole interface. So, create a 04.IHasRole.cs file, and define the interface as follows:

namespace FluentApi.Builder.Interfaces
{
    public interface IHasRole
    {
        IAmTeaching AsTeacher { get; }
        IAmStudying AsStudent { get; }
    }
}

We know, from the Sketch, that the IHasRole interface should have the two properties AsTeacher and AsStudent. And every one of these properties should return something different according to the following step on the sketch.


Moving to the next step, let’s define the IAmStudying interface. So, create a 05.IAmStudying.cs file, and define the interface as follows:

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IAmStudying
    {
        IHasStudyingSchedule Studying(params Subject[] subjects);
    }
}

We know, from the Sketch, that the IAmStudying interface should have the method Studying(subjects). This method should expect an input of type array of Subject. So, we need to define the class Subject.

Also, the Studying(subjects) should return something exposing WithSchedule(subjectsSechedules).

So, we create a Subject.cs file inside the Dtos folder and the code would be as follows:

using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public sealed class Subject : IEquatable<Subject>
    {
        public Subject(string name)
        {
            Name = name;
        }

        public Subject(Subject other)
        {
            if (other != null)
            {
                Name = other.Name;
            }
        }

        public string Name { get; }

        public bool Equals(Subject other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;

            return Name == other.Name;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;

            return Equals((Subject)obj);
        }

        public override int GetHashCode()
        {
            return (Name != null ? Name.GetHashCode() : 0);
        }

        public static bool operator ==(Subject left, Subject right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Subject left, Subject right)
        {
            return !Equals(left, right);
        }
    }

    public static class SubjectExtensions
    {
        public static IEnumerable<Subject> Clone(this IEnumerable<Subject> subjects)
        {
            return (subjects != null)
                ? subjects
                  .Where(s => s != null)
                  .Select(s => new Subject(s))
                : new List<Subject>();
        }
    }
}

What to notice here:

  1. It only has one Name property.
  2. It is immutable.
  3. It inherits the IEquatable<Subject> interface and we generated all the required members.
  4. We defined the constructor public Subject(Subject other) to provide a way of cloning the Subject from another Subject. The cloning capability in the Builder Pattern is so important because at every step you need to deal with a totally separate object (with different reference) than the ones on previous and next steps.
  5. We also defined the extension method Clone to IEnumerable<Subject> to avoid repeating the same code on different places.
  6. Inside the extension method, we are making use of the public Subject(Subject other) constructor we defined in the Subject class.

Moving to the next step, let’s define the IAmTeaching interface. So, create a 05.IAmTeaching.cs file, and define the interface as follows:

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IAmTeaching
    {
        IHasTeachingSchedule Teaching(Subject subject);
    }
}

We know, from the Sketch, that the IAmTeaching interface should have the method Teaching(subject). This method should expect an input of type Subject.

Also, the Teaching(subject) should return something exposing WithSchedule(sechedules).


Moving to the next step, let’s define the IHasStudyingSchedule interface. So, create a 06.IHasStudyingSchedule.cs file, and define the interface as follows:

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IHasStudyingSchedule
    {
        ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules);
    }
}

We know, from the Sketch, that the IHasStudyingSchedule interface should have the method WithSchedule(subjectsSchedules). This method should expect an input of type array of SubjectSchedule.

Also, the WithSchedule(subjectsSchedules) should return something exposing the method Build().

So, we create Schedule.cs and SubjectSchedule.cs files inside the Dtos folder and the code would be as follows:

using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public class Schedule
    {
        public Schedule(DateTime from, DateTime to)
        {
            From = from;
            To = to;
        }

        public Schedule(Schedule other)
        {
            if (other != null)
            {
                From = other.From;
                To = other.To;
            }
        }

        public DateTime From { get; }
        public DateTime To { get; }
    }

    public static class ScheduleExtensions
    {
        public static IEnumerable<Schedule> Clone(this IEnumerable<Schedule> schedules)
        {
            return (schedules != null)
                ? schedules
                  .Where(s => s != null)
                  .Select(s => new Schedule(s))
                : new List<Schedule>();
        }
    }
}

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos
{
    public class SubjectSchedule
    {
        public SubjectSchedule(Subject subject, Schedule schedule)
        {
            Subject = subject;
            Schedule = schedule;
        }

        public SubjectSchedule(SubjectSchedule other)
        {
            if (other != null)
            {
                Subject = new Subject(other.Subject);
                Schedule = new Schedule(other.Schedule);
            }
        }

        public Subject Subject { get; }
        public Schedule Schedule { get; }
    }

    public static class SubjectScheduleExtensions
    {
        public static IEnumerable<SubjectSchedule> Clone(this IEnumerable<SubjectSchedule> subjectsSchedules)
        {
            return (subjectsSchedules != null)
                ? subjectsSchedules
                  .Where(s => s != null)
                  .Select(s => new SubjectSchedule(s))
                : new List<SubjectSchedule>();
        }
    }
}

Here we follow the same rules as in the Subject class.


Moving to the next step, let’s define the IHasTeachingSchedule interface. So, create a 06.IHasTeachingSchedule.cs file, and define the interface as follows:

using FluentApi.Builder.Dtos;

namespace FluentApi.Builder.Interfaces
{
    public interface IHasTeachingSchedule
    {
        ICanBeBuilt WithSchedule(params Schedule[] schedules);
    }
}

We know, from the Sketch, that the IHasTeachingSchedule interface should have the method WithSchedule(schedules). This method should expect an input of type array of SubjectSchedule.

Also, the WithSchedule(schedules) should return something exposing the method Build().


Moving to the next step, let’s define the ICanBeBuilt interface. So, create a 07.ICanBeBuilt.cs file, and define the interface as follows:

using FluentApi.Builder.Dtos.Descriptors;

namespace FluentApi.Builder.Interfaces
{
    public interface ICanBeBuilt
    {
        MemberDescriptor Build();
    }
}

We know, from the Sketch, that the ICanBeBuilt interface should have the method Build() which returns the final composed MemberDescriptor.

So, we create a SubjectSchedule.cs file inside the Dtos>Descriptors folder.

This MemberDescriptor class should expose all the details of a member whether he is a Teacher or Student.


MemberDescriptor

namespace FluentApi.Builder.Dtos
{
    public enum MemberRole
    {
        Teacher = 1,
        Student = 2
    }
}

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class MemberDescriptor
    {
        public MemberDescriptor(MemberDescriptor other = null)
        {
            if (other != null)
            {
                Name = other.Name;
                Age = other.Age;
                Role = other.Role;
            }
        }

        public string Name { get; set; }
        public int Age { get; set; }
        public MemberRole Role { get; set; }

        public virtual MemberDescriptor Clone()
        {
            return new MemberDescriptor(this);
        }
    }
}

What to notice here:

  1. The MemberDescriptor class is exposing the basic info about a member. The more specific info about a Teacher or Student would reside in other two classes for Teacher and Student.
  2. The class is not immutable and that’s because on every step of the creational process, you would be adding a small detail to the object. So, you don’t have all the details at once. However, you can still choose to make it immutable but you would need to provide more than one constructor that matches your needs for every step.
  3. Still we are providing the public MemberDescriptor(MemberDescriptor other = null) constructor for cloning purposes as explained before.
  4. And we added a public virtual MemberDescriptor Clone() method for an important reason. At some steps on the process, you would be merging from a more specific case to a more generic one. In this kind of cases, your implementations of the interfaces would need to deal with the parent MemberDescriptor class, not any of its children. And, it would need to clone the entity without knowing it is originally a Teacher or Student.

For example, in this merging step:

When implementing the ICanBeBuilt interface, it would be expecting an instance of MemberDescriptor , it can’t be a specific descriptor for a Teacher or Student as it is a common step for both paths. Additionally, you would need at the end to clone the passed in MemberDescriptor.

TeacherDescriptor

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class TeacherDescriptor : MemberDescriptor
    {
        public TeacherDescriptor(MemberDescriptor member = null) : base(member)
        {
            if (member is TeacherDescriptor teacher)
            {
                Subject = teacher.Subject != null ? new Subject(teacher.Subject) : null;

                Schedules = teacher.Schedules != null
                    ? teacher.Schedules.Clone().ToList()
                    : new List<Schedule>();
            }
        }

        public Subject Subject { get; set; }
        public List<Schedule> Schedules { get; set; } = new List<Schedule>();

        public override MemberDescriptor Clone()
        {
            return new TeacherDescriptor(this);
        }
    }
}

What to notice here:

  1. Inside the cloning constructor, we need to check for null properties because as explained before, details are added piece by piece on more than one step.
  2. We are also using the IEnumerable<Schedule> extension method for cloning.
  3. We defined an override to the Clone method and now we are using our type-specific clone constructor.

StudentDescriptor

using System.Collections.Generic;
using System.Linq;

namespace FluentApi.Builder.Dtos.Descriptors
{
    public class StudentDescriptor : MemberDescriptor
    {
        public StudentDescriptor(MemberDescriptor member = null) : base(member)
        {
            if (member is StudentDescriptor student)
            {
                Subjects = student.Subjects != null
                    ? student.Subjects.Clone().ToList()
                    : new List<Subject>();

                SubjectsSchedules =
                    student.SubjectsSchedules != null
                        ? student.SubjectsSchedules.Clone().ToList()
                        : new List<SubjectSchedule>();
            }
        }

        public List<Subject> Subjects { get; set; } = new List<Subject>();
        public List<SubjectSchedule> SubjectsSchedules { get; set; } = new List<SubjectSchedule>();

        public override MemberDescriptor Clone()
        {
            return new StudentDescriptor(this);
        }
    }
}

Following the same concept as in TeacherDescriptor.


Interfaces Implementations

Now, we move to implementing our interfaces.

Let’s define the MemberBuilder class implementing the IMemberBuilder interface. So, create a 01.MemberBuilder.cs file, and define the class as follows:

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    public class MemberBuilder : IMemberBuilder
    {
        public IHuman New => new Human(new MemberDescriptor());
    }
}

The New property, should return an IHuman interface. So, we would now move to implementing the IHuman interface but we need to keep in mind something important. We need to keep passing around the partially composed MemberDescriptor because each step would add some detail to it till it is finally complete.

On the MemberBuilder class, we don’t have any detail to add, however, this is our starting point, so the class should create the initial MemberDescriptor to start with and then pass it to the next step.


Moving on to define the Human class implementing the IHuman interface. So, create a 02.Human.cs file, and define the class as follows:

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class Human : IHuman
    {
        private readonly MemberDescriptor m_Descriptor;

        public Human(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHaveNameAndCanHaveAge WithName(string name)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Name = name };
            return new HaveNameAndCanHaveAge(clone);
        }

        public IHaveAgeAndCanHaveName WithAge(int age)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Age = age };
            return new HaveAgeAndCanHaveName(clone);
        }
    }
}

We defined a constructor which takes in a MemberDescriptor and saves it to a local read-only variable.

We also implemented the two methods but what is important to notice here is that before adding any detail to the MemberDescriptor we first create a clone of it. To create a clone, we can use the cloning constructor or call the Clone method on the MemberDescriptor class.

Every method would return a different interface, so now we need to move to implementing these interfaces.


Moving on to define the HaveAgeAndCanHaveName class implementing the IHaveAgeAndCanHaveName interface. So, create a 03.HaveAgeAndCanHaveName.cs file, and define the class as follows:

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HaveAgeAndCanHaveName : IHaveAgeAndCanHaveName
    {
        private readonly MemberDescriptor m_Descriptor;

        public HaveAgeAndCanHaveName(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasRole WithName(string name)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Name = name };
            return new HasRole(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.


Moving on to define the HaveNameAndCanHaveAge class implementing the IHaveNameAndCanHaveAge interface. So, create a 03.HaveNameAndCanHaveAge.cs file, and define the class as follows:

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HaveNameAndCanHaveAge : IHaveNameAndCanHaveAge
    {
        private readonly MemberDescriptor m_Descriptor;

        public HaveNameAndCanHaveAge(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasRole WithAge(int age)
        {
            var clone = new MemberDescriptor(m_Descriptor) { Age = age };
            return new HasRole(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.


Moving on to define the HasRole class implementing the IHasRole interface. So, create a 04.HasRole.cs file, and define the class as follows:

using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasRole : IHasRole
    {
        private readonly MemberDescriptor m_Descriptor;

        public HasRole(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IAmTeaching AsTeacher =>
            new AmTeaching(new TeacherDescriptor(m_Descriptor) { Role = MemberRole.Teacher });

        public IAmStudying AsStudent =>
            new AmStudying(new StudentDescriptor(m_Descriptor) { Role = MemberRole.Student });
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.


Moving on to define the AmStudying class implementing the IAmStudying interface. So, create a 05.AmStudying.cs file, and define the class as follows:

using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class AmStudying : IAmStudying
    {
        private readonly StudentDescriptor m_Descriptor;

        public AmStudying(StudentDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasStudyingSchedule Studying(params Subject[] subjects)
        {
            var clone = new StudentDescriptor(m_Descriptor) { Subjects = subjects.AsEnumerable().Clone().ToList() };
            return new HasStudyingSchedule(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.

What to notice here is that the constructor is expecting a StudentDescriptor not a MemberDescriptor and that’s because at the moment of constructing AmStudying it is clear.

Also, notice that we even cloned the passed in array of Subject using the extension method we created before. This way we make sure that any changes to be applied by the end user to the passed in array of Subject would not affect our builders state.

If, for some reason, this is not what you intend to do, then you can change this code by passing in the passed in array as it is.


Moving on to define the AmTeaching class implementing the IAmTeaching interface. So, create a 05.AmTeaching.cs file, and define the class as follows:

using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class AmTeaching : IAmTeaching
    {
        private readonly TeacherDescriptor m_Descriptor;

        public AmTeaching(TeacherDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public IHasTeachingSchedule Teaching(Subject subject)
        {
            var clone = new TeacherDescriptor(m_Descriptor) { Subject = new Subject(subject) };
            return new HasTeachingSchedule(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.

What to notice here is that the constructor is expecting a TeacherDescriptor not a MemberDescriptor and that’s because at the moment of constructing AmTeaching it is clear.

Also, here we are not passing the same Subject passed in by the end user, we are passing a clone.


Moving on to define the HasStudyingSchedule class implementing the IHasStudyingSchedule interface. So, create a 06.HasStudyingSchedule.cs file, and define the class as follows:

using System;
using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasStudyingSchedule : IHasStudyingSchedule
    {
        private readonly StudentDescriptor m_Descriptor;

        public HasStudyingSchedule(StudentDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules)
        {
            if (m_Descriptor.Subjects.Any(s => !subjectsSchedules.Select(ss => ss.Subject).Contains(s)))
            {
                throw new ArgumentException("Some of the registered subjects are not scheduled.");
            }

            if (subjectsSchedules.Select(ss => ss.Subject).Any(s => !m_Descriptor.Subjects.Contains(s)))
            {
                throw new ArgumentException("Some of the scheduled subjects are not registered.");
            }

            var clone = new StudentDescriptor(m_Descriptor)
            {
                SubjectsSchedules = subjectsSchedules.AsEnumerable().Clone().ToList()
            };

            return new CanBeBuilt(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.

What to notice here is that we added some asserts to check if some of the registered subjects are not scheduled or some of the scheduled subjects are not registered. This is just an example and for sure you can add all your business rules as well in every step whenever needed.


Moving on to define the HasTeachingSchedule class implementing the IHasTeachingSchedule interface. So, create a 06.HasTeachingSchedule.cs file, and define the class as follows:

using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class HasTeachingSchedule : IHasTeachingSchedule
    {
        private readonly TeacherDescriptor m_Descriptor;

        public HasTeachingSchedule(TeacherDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public ICanBeBuilt WithSchedule(params Schedule[] schedules)
        {
            var clone = new TeacherDescriptor(m_Descriptor)
            {
                Schedules = schedules.AsEnumerable().Clone().ToList()
            };

            return new CanBeBuilt(clone);
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.


Moving on to define the CanBeBuilt class implementing the ICanBeBuilt interface. So, create a 07.CanBeBuilt.cs file, and define the class as follows:

using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;

namespace FluentApi.Builder.Implementations
{
    internal class CanBeBuilt : ICanBeBuilt
    {
        private readonly MemberDescriptor m_Descriptor;

        public CanBeBuilt(MemberDescriptor descriptor)
        {
            m_Descriptor = descriptor;
        }

        public MemberDescriptor Build()
        {
            return m_Descriptor.Clone();
        }
    }
}

Following the same pattern, we created the constructor, implemented the method, created the clone, added the detail, returned the new object passing in the clone to the constructor.

What to notice here is that the constructor is expecting a MemberDescriptor as at this point the passed in MemberDescriptor could be a TeacherDescriptor or a StudentDescriptor.

Also, on the Build method we are returning a clone of the descriptor but this time we can’t use the cloning constructor as if you use the cloning constructor of the MemberDescriptor class, you would finally return an instance of MemberDescriptor, neither a TeacherDescriptor nor a StudentDescriptor which is not right. Instead, we use the Clone method which would return the right instance at run-time.


Time For Testing

Now, with a simple Console application we can try to run the following code:

using System;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Implementations;

namespace FluentApi
{
    class Program
    {
        static void Main(string[] args)
        {
            var memberBuilder = new MemberBuilder();

            var ahmed =
                memberBuilder
                    .New
                    .WithName("Ahmed")
                    .WithAge(36)
                    .AsTeacher
                    .Teaching(new Subject("Software Engineering"))
                    .WithSchedule(
                        new Schedule
                        (
                            new DateTime(2021, 11, 20),
                            new DateTime(2021, 12, 20)
                        ), new Schedule
                        (
                            new DateTime(2022, 1, 5),
                            new DateTime(2021, 3, 5)
                        ))
                    .Build();

            var subjectsToStudy = new Subject[]
            {
                new Subject("Software Engineering"),
                new Subject("Physics")
            };

            var mohamed =
                memberBuilder
                    .New
                    .WithAge(15)
                    .WithName("Mohamed")
                    .AsStudent
                    .Studying(subjectsToStudy)
                    .WithSchedule
                    (
                        new SubjectSchedule
                        (
                            subjectsToStudy[0],
                            new Schedule
                            (
                                new DateTime(2021, 11, 20),
                                new DateTime(2021, 12, 20)
                            )
                        ),
                        new SubjectSchedule
                        (
                            subjectsToStudy[1],
                            new Schedule
                            (
                                new DateTime(2021, 11, 20),
                                new DateTime(2021, 12, 20)
                            )
                        )
                    )
                    .Build();

            Console.ReadLine();
        }
    }
}

What you can notice here:

  1. Our Fluent API is working as it should.
  2. May be for this example, you might find it an overkill to create a Fluent API for such simple objects, however, we are using this simple example for demonstration purposes only.

Final Words

The Builder Design Pattern has some advantages but it also adds complexity. Therefore, you need to use it only when you actually need it.

That’s it, hope you found reading this article as interesting as I found writing it.


Also Published Here


Written by ahmedtarekhasan | .NET (DotNet) Software Engineer & Blogger | https://linktr.ee/ahmedtarekhasan
Published by HackerNoon on 2023/01/26