Introduction
In these modern days, writing code is not difficult. However, writing clean and scalable code is not easy. In this article, we will talk about some tips to write clean C# code for our projects. At first glance, any developer that has never seen your code before has to understand it as much as possible. Each class should tell a story; it helps us to understand the code better.
Here is a list of important tips to write clean C# code.
Use a Good IDE
First things first, choose the Best IDE Available for your Tech Stack. In our case, Visual Studio is one of the most popular and betters IDEs for C#. It is a solid and completely FREE product of Microsoft. Some developers prefer Rider IDE as well (paid). Working with these IDEs makes sure that your code remains tidy. Visual Studio has pretty stable Intellisense features that can correct and suggest changes in code. You can find a detailed tutorial on how to install here.
Use Meaningful Names
Naming variables is probably the hardest part of the entire software development life cycle. Thinking of meaningful names for your variables and methods is quite time-consuming. But skipping this process and giving random names is not a good idea either, is it?
Bad Practice
int d;
This is the easiest way to name variables right? But DO NOT do it. A good name helps other developers to understand the context and usage of the variable/method. Here is how you would want to name your variables.
Good Practice
int daysToAppocalypse;
Use Camel/Pascal Case Notation
Apart from choosing an appropriate name for your variables, also maintain how you write the names. Ideally, we use Camel Case and Pascal Case Notation as best code practices. Do not use random Capitals throughout your variables. That just doesn’t look pretty!
Camel Case Notation
Basically, the first letter of the first word of the variable will be in lower case, and the first letter of every other word that follows should be in upper case. You will have to use this notation while naming your local variables and method arguments.
Bad Practice
int RandomInteger;
string FirstName;
Good Practice
int randomInteger;
string firstName;
Pascal Case Notation
Here, the first letters of all your words should be in Upper Case. We use this kind of notation for naming Methods and Classes
Bad Practice
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
Good Practice
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
Pay Attention to Formatting
Formatting your code improves code readability. Tabs over Spaces, Remember?
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
How does this look? Pretty annoying, yeah? Now, Visual Studio has a built-in feature to format your code perfectly. To do this, go to the concerned class and simply Press CTRL + K and CTRL + D. See it? Cool, yeah?
Add Comments Whenever Necessary
This is something we all developers hate, don’t we? 😀 However, adding a few lines of comments/description of what the method actually does helps you and the other developers in the longer run. Visual Studio makes it much easy for you. Simply Go above the concerned method and type in ///, VS automatically generates a comment template for you including the arguments of the method.
Now, why is this cool? Whenever you call this method (from anywhere), Visual Studio shows your comment too. Trust me, it is very helpful.
P.S. Add comments only when the situations demand you to. For example, when a particular method is too complex and requires an in-depth explanation. This is one case where you would like to add comments. Remember that maintaining comments will become a task as well, so use comments sparingly.
Reuse Code
Writing code that can be reusable is quite vital. It can decrease the overall lines of code in your project and makes it highly efficient. You do not want to copy-paste a function through multiple classes. Rather what you could do is, make a Shared Library Project and reference it in each of the required projects. This way, we build reusable functions. And, if there is any modification needed, you would just have to change the code in the shared library, not everywhere.
Keep Class Size Small
According to the Solid Principles, you must segregate classes to small blocks which has a single responsibility function only. This helps us to achieve loosely coupled code. Make sure that you don't need to scroll over and over while viewing a class. This can be a general rule of thumb.
Use Design Patterns
This may be something to do for an architect level developer. It takes quite a lot of experience to determine what kind of design patterns to apply to which scenario. Design patterns are basically patterns that can provide a reusable solution while architecting solutions.
Structure your Solution Well
Do you build Structured Solutions? It is highly satisfying and important to build one. Here is one of my on-going solutions following an Onion Architecture.
You understand the point. It is still possible to do everything within a single project. But, in order to favor scalability and loosely couple the solutions, we split them up to various layers like Application, Domain, Infrastructure, and so on.
Advantages
- Reusability: If you want to use the same Project for another solution, you could do so.
- Improved Security: High-quality code is less likely to contain vulnerabilities such as buffer overflows, injection attacks, or improper access control. Code that follows best practices is more resilient against common security threats.
- Highly Maintainable: Well-structured code is easier to maintain. When changes or updates are needed, developers can make modifications with confidence, knowing they are less likely to introduce bugs.
- Scalable: Clean code is more scalable. As a project grows, maintaining clean code becomes increasingly important. It allows for the addition of new features and changes without causing excessive technical debt.
Avoid Magic Strings/Numbers
What are Magic Strings? They are strings that are directly specified within the application code which has a direct impact on the application behavior. In other words, do not use hardcoded strings or values in our application. It would be tough to keep track of such strings when your application grows. Also, these strings can be associated with some kind of external references, like a file name, file path, URL, etc. In such cases, when then the location of the resources change, all these magic strings would have to be updated, or else that application breaks. Consider the following example:
if(userRole == "Admin")
{
//logic here
}
Instead of this, you could write the following
const string ADMIN_ROLE = "Admin"
if(userRole == ADMIN_ROLE )
{
//logic here
}
Alternatively, you could also create an Enum for the User Roles and simply use it. This a much cleaner way to write code.
Remove Unused Code
There is often a practice of commenting out the unused code. This ultimately increases the lines of code when the application gets compiled. You do not want to do this. You could use source controls like Git to make sure that you can revert back at any time. Prefer using Git over commenting out code.
Use Method Chaining
This is a common technique used extensively in the default generated codes by Microsoft. Here, each method returns an object and these functions will be chained together in a single line. Recognize it? This is a good example of method chaining.
services.AddHealthChecks().AddSqlServer(_configuration.GetConnectionString("DefaultConnection"));
Here is a detailed example. We have a student class and another random method that creates and returns a data-filled student object.
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
public Student SomeMethod()
{
Student testStudent = new Student();
testStudent.Name = "Jay Krishna Reddy";
testStudent.Age = 25;
return testStudent;
}
Setting the values to the Student object may not be an issue for us developers. But, let’s say a unit test developer has to test on your class and really doesn’t get C#, or you want to please your client by making this whole process simple. This is where Fluent Interfaces come in. Create a new Fluent class as shown below:
public class StudentFluent
{
private Student student = new Student();
public StudentFluent AddName(string name)
{
student.Name = name;
return this;
}
public StudentFluent AddAge(int age)
{
student.Age = age;
return this;
}
}
public StudentFluent SomeMethod()
{
return new StudentFluent().AddName("Jay Krishna Reddy").AddAge(25);
}
This makes so much sense and improves the readability on a whole new level, right? Another simpler example of method chaining is as follows:
public string AnotherMethod()
{
string name = "Jay Krishna";
return name
.Replace("J", "A")
.Replace("A", "J")
.Replace(".", string.Empty);
}
Use Async/Await
Asynchronous programming is the way to go! Asynchronous Programming helps improve the overall efficiency while dealing with functions that can take some time to finish computing. During such function executions, the complete application may seem to be frozen to the end-user. This results in bad user experience. In such cases, we use async methods to free the main thread.
Read more here.
Don’t use ‘throw ex’ in the catch block
You really don’t want to just ‘throw ex’ the exception after catching it and lose the stack trace data. Just use ‘throw’. By using this, you are able to store the stack trace as well, which is kind of vital for diagnostics purposes.
Bad Practice
try
{
// Do something...
}
catch (Exception ex)
{
throw;
}
Good Practice
try
{
// Do something..
}
catch (Exception ex)
{
throw;
}
Use Ternary Operator
Consider the following example. I am sure that many of you still follow this practice.
public string SomeMethod(int value)
{
if (value == 10)
{
return "Value is 10";
}
else
{
return "Value is not 10";
}
}
But what if there is a better and cleaner way? Introducing Ternary Operators.
Now the multiple lines of code that we wrote earlier can be shrunk to just one line using the ternary operators. You can start imagining how many lines of code this is going to save!
public string SomeMethod(int value)
{
return value == 10 ? "Value is 10" : "Value is not 10";
}
Use Null Coalescing Operator
Similarly, we have yet another operator that can come in handy while you do null checks. ?? This operator is known as Null Coalescing Operator in C#. Read more here.
Consider another example. Here is a small function that takes in a Student object as a parameter and checks for the null object. If null, return a new Object with data, or else return the same object.
public Student SomeMethod(Student student)
{
if (student != null)
{
return student;
}
else
{
return new Student() { Name = "Jay Krishna Reddy" };
}
}
Let’s add the operator and shrink this function too!
public Student SomeMethod(Student student)
{
return student ?? new Student() { Name = "Jay Krishna Reddy" };
}
Prefer String Interpolation
Every time you want to add dynamic values to strings, we preferred composite formatting or simply adding them with a plus operator.
public string SomeMethod(Student student)
{
return "Student Name is " + student.Name + ". Age is " + student.Age;
}
Starting with C# 6, the String Interpolation feature was introduced. This provides a more readable and cool syntax to create formatted strings. Here is how you use interpolated strings.
public string SomeMethod(Student student)
{
return $"Student Name is {student.Name}. Age is {student.Age}";
}
Use Expression Bodied Methods
Such methods are used in scenarios where the method body is much smaller than even the method definition itself. Why waste Brackets and lines of code, right? Here is how you would write an Expression Bodied Method.
public string Message() => "Hello World!"; public
Read more about Expression Bodied Methods here.
Avoid Too Many Parameters
Too many parameters is always a nightmare. If you tend to have more than 3 parameter inputs to any method, why not wrap it into a request object or something and then pass? Let’s see a small example.
public Student SomeMethod(string name, string city, int age, string section, DateTime dateOfBirth)
{
return new Student()
{
Age = age,
Name = name,
// Other parameters too
};
}
You would probably want it to be like this. Get the idea?
public Student SomeMethod(Student student)
{
return student;
}
Don’t Ignore Caught Errors
This is something I kept on doing. There is a good chance that many of you too do this. We add a try-catch block and just ignore the error handling, right? It’s a good practice to handle such errors and log them to a table or disk.
public void SomeMethod()
{
try
{
DoSomething();
}
catch
{
// Handle exceptions here if needed
}
}
public void SomeMethod()
{
try
{
DoSomething();
}
catch (Exception ex)
{
LogItSomewhere(ex);
}
}
If you found this article helpful.
Keep learning!