Builder Design Pattern in .NET C#
top of page

Subscribe to get best practices, tutorials, and many other cool things directly to your email inbox

  • Writer's pictureAhmed Tarek

Builder Design Pattern in .NET C#

Updated: Apr 17

Step by step guide to develop a Fluent API from scratch in .NET C# using the Builder Design Pattern.


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Callum Hill on Unsplash

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.


 

Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Mikael Seegen on Unsplash

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:


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering

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


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering

And this:


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering

 


 

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:


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering

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.



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:



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:



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:



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:



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:



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:



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:



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:



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:




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:



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:



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




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


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering

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



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



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:



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:



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:



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:



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:



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:



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:



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:



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:



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:



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:



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.


Step by step guide to develop a Fluent API from scratch in DotNet (.NET) CSharp (C#) using the Builder Design Pattern. Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Ray Hennessy on Unsplash

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.



Recent Posts

See All

Subscribe to get best practices, tutorials, and many other cool things directly to your email inbox

bottom of page
Mastodon Mastodon