top of page

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

  • Writer's pictureAhmed Tarek

Design Best Practices In .NET C#

Updated: Dec 27, 2023

Some Design Best Practices to follow in .NET C#


Design Best Practices In DotNet (.NET) CSharp (C#). Dependency Injection (DI) Inversion of Control (IoC) IoC Containers Delegate Interface Event Snapshot Immutable Strategy Pattern Strategy State Behavior Software Architecture Engineering Development Code Coding Programming Paging
Photo by Immo Wegmann on Unsplash, adjusted by Ahmed Tarek

After working for some reasonable period of time in the Software field, day by day you would get more confident that there is still something new you are not aware of, yet.


Depending on your exposure to different kinds of projects and requirements, you would harness new skills. However, in my humble opinion, the most precious skills are the Analytical and Design skills.


Don’t get me wrong, we all know that at the end of the day there is some code to write and someone would eventually do it. However, writing the code is something you could learn from an online -or offline- documentation or a tutorial. On the other hand, Analytical and Design skills are different…


In this article we are going to explore some Design Best Practices that are practically proved to be efficient.


 

Unified Return Object


You know when you find yourself in the need to describe the data returned by a method? We call this Meta-Data as this kind of information is not the data itself, but some important information which could be used later -by the same module or some other module- to understand the actual data or to know how to process the data.


Sometimes it is sound and clear that you have some Meta-Data to define and propagate through the system, and sometimes it is a bit tricky.


One of the famous examples of the obvious cases of using Meta-Data is the node object in a tree structure. Beside the information about the node name, id,… and location in the tree structure, you know that each node should eventually hold some information about some entity in the business hierarchy.


However, sometimes it is not that clear that you have Meta-Data.


When calling a void RenameFile(string newName) method passing in a new name and not expecting any returns. However, in the real world this could be a problem because may be on the File System where this file is persisted, this newName already exists. In this case, the method needs to return something to inform the caller about this.


I can hear someone saying “Let’s return as string of the new name” or another one saying “Let’s throw an exception in this case”.


These solutions might work, however, the best thing to do here is to return a unified object with some data describing what actually happened. Something like:



Some of the benefits of returning a unified object:

  1. Moving logic to where it actually belongs as most of the cases this enforces the handler of the call to do its full job and provide full info instead of illogically delegating this to the caller.

  2. Defining a more clear contract between the caller and the handler.

  3. The caller now can make accurate decisions based on the full information provided by the handler.

  4. Dealing with unified objects makes it much more easier to design common modules.

  5. And others…


If you want to see practical implementations of using Unified Return Objects, you can check Paging/Partitioning — Learn the Main Equations to Make it Easy and Better Enhanced Repository Pattern Implementation in .NET C#.


 

Don’t Abuse Dependency Injection (DI), Inversion of Control (IoC), and IoC Containers


Imagine that you are defining Car class, and inside the Accelerate method you want to log a message with the current speed of the car. You would inject your magnificent ILogger in the constructor of the class and then start using it, right?


Yes, it would work, and unfortunately, according to today’s design standards, this is perfect. However, may I ask you about something?


If we don’t have a ILogger, should the Car class be able to perform as expected? Can we say that the Car class actually depends on the ILogger to the extent that it can’t do its job without it?


The answer is simply that the Car should not depend on the ILogger. You might argue that even if it doesn’t fully depend on it, it still needs it. My answer to this would be: not exactly. The Car doesn’t even need the ILogger, what actually needs the ILogger is the main application which is aware of both the Car and the ILogger. The main application needs to get some info from the Car and then start using the Ilogger to log this info.


Therefore, the right design here is to completely remove this dependency and start implementing Events. So, in our example, the Car class needs to define a CarAccelerationChanged event and the main application should subscribe to it.


If you want to read more about this, you can check When Not To Use DI, IoC, and IoC Containers.


 

Delegating Controlling How To Fire Events


Now, you listened to the previous advice, defined the great CarAccelerationChanged event inside the Car class, now let’s see how you implemented it.



Great, it is working fine. However, what if now you have a new type of cars, a GhostCar. This GhostCar is a Car but it is silent, it should not report its acceleration.


In this case, you would need to do something like this:



You have to do some modifications on the Car class, you had to copy some logic from there and that’s because you just want to stop firing the CarAccelerationChanged event.


You could have avoided this from the beginning by following one Best Practice. You should have defined your Car class as follows:



 

The Snapshot Structure


Keeping going on with our Car class, now you are building a Tracker module which should track the Car acceleration. It is obvious now that the Tracker module would subscribe to the CarAccelerationChanged event as follows:



We have a problem, the Car was already moving with a steady acceleration before creating the Tracker. This means that at the moment of creating an instance of the Tracker, the ShowOnScreen(CurrentAcceleration) call on line 13 would show Zero on the screen although this is not true.


To fix this problem, you would need your Car class to expose an acceleration property and then use this property to get the initial value of the acceleration on line 13.


However, what if it is not only about the Acceleration? What if you have more properties to reflect the Car state at a certain moment of time? Creating properties for all of these inside the Car class is not good.


There is a Best Practice for this called the Snapshot. So, to apply this, the code should look like this:



Following this pattern:

  1. We defined a CarState class to represent the state of a Car at a certain moment of time in terms of Acceleration and Temperature. Note that it is immutable.

  2. It also implemented IEquatable<CarState> so that at any moment we might like to compare two states at different moment of times to see if the state has changed or not. For your info, if you are using Visual Studio, you can easily generate these members using some keyboard shortcuts (on my VS keyboard scheme, it is Alt + Insert).

  3. We defined public CarState Snapshot inside the Car class.

  4. In the Accelerate method, we are now updating the Snapshot and using it elsewhere whenever needed.

  5. Now, inside the Tracker class, we are using the Snapshot to initially get the Acceleration value beside subscribing to the CarAccelerationChanged event.


Now, this is perfect.


 

Immutability


Long story short, immutability is good, let’s do more of it 🙂


Just kidding, to know why immutability is good, let’s see:

  1. You keep the state of the object safe, away from tampering.

  2. You can depend on that and do a lot of things like comparisons to check states.

  3. You can easily write unit tests.

  4. You can write clean code.

  5. You save yourself the hassle of copying objects around to maintain different state before applying changes.

  6. And others…


Therefore, as we did on the CarState class, this is the way of making a class immutable.



Worth to mention here, starting from C# 9.0 we have something called Record. I will leave it to you to search about it.


I wrote a more detailed article about Immutability which I really encourage you to check. This is the article; Why Immutability Is Important in .NET C#.


 

Paging and Partitioning


Paging is one of the patterns that is widely used but almost no one is talking about it in terms of design practices. In simple words, nothing is totally absolute.


When you are creating an API, whether it is a REST API or not, you have to control the amount of data flowing through your API. Yes, you can say that you have no limits, but actually, no. You have limits but these limits are so high that you missed them.


Therefore, what I always advise developers to do when they are designing an API, is to set a throttling strategy, and to design the solution so that their API and throttling strategy work in harmony.


For example, if you have a GetAllEmployees API, you should allow the caller to get all employees only if they are less than 10,000. However, if someday the system has more than 10,000 Employees, this is the right time to return the first 10,000 Employee only and provide the caller with a Meta-Data object telling him what happened and how to get the next 10,000 and so on,… and this is Paging my friend.


If you want to read more about paging, you can check Paging/Partitioning — Learn the Main Equations to Make it Easy.


If you want to see a practical example of how to use paging in designing APIs, you can check Better Enhanced Repository Pattern Implementation in .NET C#.


 

Delegates Over Func<>


Most of the time when I want to define a reference to a method, I prefer to define a delegate and then start using it.



The difference between both ways is that with the defined delegate, you get better intellisense and autocomplete support


Design Best Practices In DotNet (.NET) CSharp (C#). Dependency Injection (DI) Inversion of Control (IoC) IoC Containers Delegate Interface Event Snapshot Immutable Strategy Pattern Strategy State Behavior Software Architecture Engineering Development Code Coding Programming Paging

Rather than the other way which would end up with you to something like this


Design Best Practices In DotNet (.NET) CSharp (C#). Dependency Injection (DI) Inversion of Control (IoC) IoC Containers Delegate Interface Event Snapshot Immutable Strategy Pattern Strategy State Behavior Software Architecture Engineering Development Code Coding Programming Paging

 

String.GetHashCode Nightmare


Have you ever imagined that String.GetHashCode could drive you crazy? If not, it happened my friend.


While working on a side project, I had an annoying issue which was driving me crazy. It took me a while to understand what was going on an finally I discovered that it was caused by String.GetHashCode.


Long story short, if you are going to persist the value returning from String.GetHashCode to be used in the future in different application run session, you should do it in a different way.


If you want to understand the problem and how to fix it, you would need to check the article When String.GetHashCode() in .NET C# Drives You Crazy.


 

Separate State From Behavior


In the old days, we used to explain the meaning of a Class in Object Oriented Programming (OOP) Languages as the template used to create an object. We used to say that this template defines the state and behavior on that object.


Therefore, following this concept, we used to define classes with literally everything that could belong to an object, this includes states and behavior.


However, after years of developing different kinds of software systems, this way of working was proved to be inefficient, specifically, in the Game Development field.


In the Game Development field, objects are more likely to change their state and behavior and sometime separately. Thus, it was so hard to accept that an object would always be bloated with all these variables which could change at any second. Therefore, the need to a new way of working aroused.


In simple words:

  1. State should be easily persisted, copied and duplicated.

  2. Behavior should be easily switched at runtime per need and changes.


If you want to read more about this topic, you can check the article Strategy Design Pattern In .NET C#.


 

Not Using IoC Containers is Not an Excuse


Ok, sometimes I see developers working on new projects or legacy ones and I notice that the new keyword is scattered all over the solution.


Most of the times when I ask why I get the answer:

Unfortunately, we are not using Inversion of Control (IoC) Containers.

Ok, I get this part, for some reason, you are not using Inversion of Control (IoC) Containers. But, then what?!!


This is not an excuse. Let me put it in simple words.


Developers sometimes get confused about Dependency Injection (DI), Inversion of Control (IoC), and IoC Containers.


These are three different things and sometimes they don’t come together. It is not like a package in terms of you either take it all or leave it all.


Inversion of Control (IoC) is about which module should depend on which module. Therefore, it is mainly about defining your dependencies in the right way.


Dependency Injection (DI) is about the way of injecting a dependency inside a module which depends on it.


IoC Containers is about the way of mapping a dependency abstraction to its implementation(s).


Therefore, if you are not using IoC Containers at the end of the day, this doesn’t mean that your dependencies should be floating around without any kind of planning and designing.


Yes, you would eventually need to use new but there is a big difference between using it in some isolated places and having it everywhere in the solution.


 

Wrap Static Objects and Third Parties


Most of the time the difficulties we go through while writing unit tests are caused by the static objects and third parties which we start using directly in our production code.


With static objects and third parties, you lose the benefit of using mocks and stubs and actually you are making your life so hard.


Instead, you should abstract these and wrap them into thin wrappers which you can then easily mock and stub.


If you want to see a practical example of how to do this, you can check How to Fully Cover .NET C# Console Application With Unit Tests.


 

Best Practice When Using Timers


At some occasions you find that you need to use Timers in your solution. This is ok. However, what is not ok is that you assume that you can’t have full control over your Timer.


By control, I mean to be able to fully test the modules which are using the Timer. I know some developers who deal with the Timer as a black hole. Sometimes, it is to the extent that they use loops with “Wait” instead of using Timers.


Ok, I get it, it is not straight forward but still it is not impossible. I already wrote an article with code example. If you like you can check the article Best Practice for Using Timers in .NET C#.


 

Avoid Code Hidden Intentions


Once I was talking to a friend developer and somewhere in the middle of our chat I told him that our code intentions should be clear. He stopped at this statement and said this is the first time for him to hear it.


I am not sure actually if I invented it or it was just something at the back of my mind. However, this is not the point now.


To explain what I mean by hidden intentions, let me first explain something. In a software solution, we have different modules. These modules interact with each other through contracts. These contracts represent the inputs and outputs, but this is not the whole story.


The contracts also represent some logic and the business meaning of it. So, if you have an interface with two methods like CalculateIncomeTax and CalculateVatTax, you can’t assume that the end user would just care about the input and output, no, he needs to trust that the implementer would not swap the implementation of both methods. This is something that the compiler would not be able to detect.


If you are already aware of the Liskov Substitution Principle of the SOLID principles, then you would notice that this is referring to what we are talking about.


If you want to know more about this topic, you can check the article SOLID: Liskov Substitution Principle Explained In .NET C#.


 

Defining Non-Generic Interface Beside the Generic One


Ok, generics are good, but… Would IMyInterface<TData> be enough? Do I need to define IMyInterface as well?


To understand what I actually mean by this, you need to go through a real example which I already did before on my article A Best Practice for Designing Interfaces in .NET C#.


Therefore, my advice to you is to go and check this article as it would walk you through the whole experience.


 

Design Best Practices In DotNet (.NET) CSharp (C#). Dependency Injection (DI) Inversion of Control (IoC) IoC Containers Delegate Interface Event Snapshot Immutable Strategy Pattern Strategy State Behavior Software Architecture Engineering Development Code Coding Programming Paging
Photo by Pietro Rampazzo on Unsplash, adjusted by Ahmed Tarek

Final Words


Are these all the Design Best Practices I want to share with you? Absolutely no, but, these are the most important ones and believe me, I know seniors who don’t actually know about them.


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