In the ever-evolving world of software development, writing clean and efficient code by using C# best practices is crucial. It not only enhances the maintainability of our codebase but also contributes to the overall success of our projects.
In the upcoming sections, we’ll dive deep into the practices that enhance readability and fortify code structure.
Readability and Code Structure
Readability is not just a luxury; it’s a fundamental necessity in coding. Well-structured and easily understandable code ensures smoother collaboration among developers, simplifies maintenance and reduces the likelihood of errors.
By adopting meaningful naming conventions, we create a shared language that communicates the purpose and functionality of our variables, methods, and classes. Proper indentation and formatting are visual aids that enable swift comprehension and navigation, leading to efficient coding.
Furthermore, the Single Responsibility Principle for methods reinforces the modular nature of our code, promoting clarity and maintainability. The use of curly braces imparts visual structure, enhancing the scannability of our code blocks.
Meaningful Naming Conventions
Choosing descriptive and meaningful names for variables, methods, classes, and other identifiers is a cornerstone of writing readable code. A well-named identifier means that it doesn’t need additional comments.
Embrace the PascalCase
convention for classes and methods and the camelCase
convention for variables. Furthermore, we should avoid using cryptic or abbreviated names that can confuse other developers. Following the naming convention makes our code more self-explanatory, making it easier for others to understand and maintain.
Let’s take a look at how to define meaningful names and avoid unclear and cryptic names:
public int CalculateArea(int length, int width) { return length * width; } public int Foo(int x, int y) { return x * y; }
Here, we create the CalculateArea()
method, which we name in a way that makes its purpose crystal clear. But when we check out the alternative Foo()
method, it is confusing and hard to figure out what it does!
Maintain Proper Indentation and Formatting
Consistent indentation and formatting make our code easier to scan and comprehend. Modern Integrated Development Environments (IDEs) often provide automated formatting tools that ensure a uniform style.
Additionally, we utilize appropriate spacing around operators and keywords to improve code clarity. Let’s see how to apply proper indentation and formatting:
public void ValidateNumber(int number) { if (number > 0) { Console.WriteLine(number); } if ( number > 0 ) { Console.WriteLine(number); } }
Here, we start with a neatly structured if
statement that’s straightforward to grasp. Let’s contrast it with our second if
statement, which needs proper formatting and clarity on the condition we’re trying to express.
Follow the Single Responsibility Principle in Method Design
Methods with a single responsibility are easier to understand and maintain. We should avoid creating large, complex methods that perform multiple tasks.
Breaking down our logic into smaller, focused methods is one of the best practices in C#. This improves readability and facilitates easier unit testing and code reuse.
Let’s look at the example where we define a method with a single responsibility and also avoid a method with multiple responsibilities:
public void SendEmail(string recipient, string subject, string message) { } public void ProcessOrder(Order order) { ValidateOrder(order); CalculateTotal(order); UpdateInventory(order); }
Here, we define a well-structured SendEmail()
method to showcase this concept, focusing solely on sending emails. In contrast, our ProcessOrder()
method undertakes multiple responsibilities, violating the principle, and leading to complexity.
Use Curly Braces for Clarity
While C# allows omitting curly braces for single-line control statements, such as if
statements or loops, using them consistently is one of the best practices in C#.
Let’s see an example to improve the visual clarity of our code and reduce the chances of introducing bugs when adding more statements to the block later:
if (condition) { var userId = GetUserId(); } if (condition) var userId = GetUserId();
Here, we’ve got two if
statements. In the first one, we use the curly braces to wrap the statement inside it. It makes the scope of the if
condition explicit.
In the second if
statement, we skip the curly braces. While this is allowed in C# for a single-line statement, it can lead to confusion and potential issues when we want to add more code to the if
block later.
Exception Handling and Defensive Coding
Our role as developers goes beyond mere coding; we’re architects of resilience. Exception handling empowers us to foresee potential issues, providing a resilient framework for error detection and graceful management.
However, it’s crucial to exercise precision when catching exceptions. Embracing selective exception handling ensures accurate error messages and more effective troubleshooting. Moreover, our commitment to code quality is evident in our avoidance of “magic numbers” or hard-coded values, which can complicate maintenance and undermine clarity.
Our defensive coding approach also involves consistent null
checks for objects, guarding against null
reference exceptions that can disrupt even the most robust applications. By encapsulating data within properties instead of public variables, we establish controlled access to data and reinforce a safeguard against unintended modifications.
Only Catch Exceptions That We Can Handle
Exception handling is not just about catching errors; it’s about catching the right errors. Using broad catch-all blocks is tempting, but doing so can obscure issues and hinder debugging.
By catching only the exceptions we can handle, we ensure that our error messages are accurate and relevant, making troubleshooting far more effective.
Let’s look into an example of catching a general exception type:
try { var result = 10 / 0; } catch (Exception ex) { Log.Error($"Exception occurred: {ex.FileName}"); }
In this case, we catch a general exception, sacrificing specificity and potentially masking underlying issues. While this approach might seem like a catch-all safety net, it can lead to confusion and mask underlying problems.
When we catch a general exception, we risk losing specific information about what went wrong within our code. Catching a general exception can make troubleshooting and debugging challenging, as we need to catch up on the precise cause of the error. It’s like trying to diagnose a medical condition with vague symptoms; we need detailed information to address the issue effectively.
In another case, let’s see how to catch a specific exception type:
try { var result = 10 / 0; } catch (DivideByZeroException ex) { Console.WriteLine($"Divide by zero exception: {ex.Message}"); }
Here, we catch a specific exception like the DivideByZeroException
that ensures targeted error handling and relevant error messages.
Avoid Magic Numbers and Strings
Hard-coding values directly into our code, known as “magic numbers” or “magic strings,” can make our code harder to maintain and modify. They hinder code readability and maintainability and make changes error-prone.
We can adhere to best practices in C# by leveraging constants or enums to assign meaning and significance to these values to overcome these shortcomings. By encapsulating these values within well-named constants or enum
, we provide clarity and context, making our code more self-documenting and less prone to errors
Let’s see an example to avoid using a magic number:
if (status == 1) { } const int ActiveStatus = 1; if (status == ActiveStatus) { }
In the first if
block, we use the hard-coded number, which lacks clarity and context. In contrast, we use a constant ActiveStatus
in the second if
condition, providing a clear name for the value.
Always Do Null Checks for Objects
Null reference exceptions can bring even the most robust applications to our knees. To prevent these, it’s crucial to perform null checks before accessing properties or methods of objects.
Let’s use the is
operator to do the null
check for objects, ensuring that our code gracefully handles situations where an object is null
:
List<string> users = GetUsers(); if (users is not null) { foreach (var user in users) { Console.WriteLine($"User Found: {user.Id}"); } }
We’re using the is not null
pattern with the is
operator to check if our users
list is not null before running a loop through it. It ensures we only work with valid data, avoiding nasty crashes.
Use Properties Instead of Public Variables
Encapsulation is a fundamental principle in object-oriented programming. Instead of exposing class variables directly as public
, we encapsulate them within properties. Properties offer an elegant approach to abstracting a class’s internal state and controlling how to read and modify that state.
By encapsulating class variables within properties, we create a boundary that ensures consistent and secure access to the data while preserving the integrity of the class’s behavior.
Now, let’s see a bad practice of using the public variables:
public class Student { public string Name; public int Age; }
We define a Student
class and expose its internal state through public variables like Name
and Age
. This approach violates encapsulation, as external code can directly modify these variables without any control or validation.
Following C# best practices, we avoid this by using properties instead of public variables:
public class Student { private string _name; public string Name { get { return _name; } set { _name = value.Trim(); } } }
In the class, we encapsulate the variable _name
within a property. This practice provides controlled access to data while preventing direct modifications.
String Manipulation and Collections
Strings are the building blocks of user interfaces, messages, and data representation. Our ability to manipulate them intelligently directly impacts the clarity and functionality of our applications. Collections, on the other hand, form the backbone of data organization and processing.
We leverage the power of string interpolation for an effortless combination of text and variables. Additionally, we explore the efficient usage of the string.IsNullOrWhiteSpace()
method to handle null
, whitespace, or empty conditions, allowing us to navigate the realm of enhanced string operations.
Moreover, we delve into the foundational role of collections in data organization and processing.
Concatenate Strings Using String Interpolation
String concatenation is a common task in programming, and C# offers an elegant solution through string interpolation. This technique enhances readability and simplifies the process of combining text and variables. String interpolation allows us to embed expressions directly within a string.
Let’s see how to create a seamless blend of text and variables:
var fullName = firstName + " " + lastName; var fullName = $"{firstName} {lastName}";
Here, we initially combine the two strings using the conventional method. However, we can make it more elegant by employing the dollar sign $
followed by the string, with variables enclosed in curly braces {}
. It’s a nifty trick, don’t you think?
Additionally, let’s also perform custom formatting with string interpolation:
var price = 25.99; var formattedPrice = $"Price: {price:C2}";
Here, we use the string interpolation to apply custom formatting to the price
variable, ensuring it appears as a currency value with two decimal places.
Use IsNullOrWhiteSpace() Method to Check for Null or Empty Conditions
The string.IsNullOrWhiteSpace()
method is a compact and efficient solution to the age-old problem of checking strings for null
, whitespace, or empty conditions. This method, true to its name, evaluates a string and returns true
if it’s null
, empty, or whitespace, and false
if it contains content.
Let’s take a look at an example:
var userEmail = GetUserEmail(); if (string.IsNullOrWhiteSpace(userEmail)) { Console.WriteLine("Email address is missing."); } else { Console.WriteLine("Email address found."); }
We use the string.IsNullOrWhiteSpace()
method to check if the userEmail
is either null
, empty, or just contains whitespace.
Always Use Any() Extension Method Instead of Checking the Count
The Any()
method checks for the presence of any items in the collection and returns a boolean value accordingly. It simplifies the code and enhances performance by avoiding unnecessary enumeration of the entire collection to calculate the count.
Any()
and Count()
methods, check out our great article Any() vs Count() in .NET: Which One is Better?Another advantage of using the Any()
method is its behavior with empty collections. If we call the Any()
method on an empty collection, it returns false
, eliminating the need for additional null
checks.
Let’s see an example:
List<string> tasks = GetTasks(); var hasCompletedTasks = tasks.Any(task => task.IsCompleted);
Here, we want to know whether any tasks
are marked as completed. We don’t require counting the elements or explicitly checking for zero counts, resulting in cleaner and more efficient code.
Code Quality
We aspire to create code that surpasses the realm of error-free execution and achieves exceptional standards of readability, efficiency, and design elegance.
From leveraging implicitly typed variables for concise yet clear declarations, similarly, we embrace the null-conditional operator’s power for robustness, ensuring readability and reliability. Other best practices in C# such as comprehensive documentation, unit testing, and rigorous input validation further solidify our commitment to quality.
Implicitly Typed Local Variables
C# allows us to use the var
keyword for type inference when declaring variables and implicit typing for local variables when the variable type is evident from the right side of the assignment. We should avoid using the var
keyword unless the variable’s type is evident from the right side of the assignment.
Let’s take a look at an example where we use var
correctly and excessively:
var employeeName = "John Doe"; var x = GetSomething();
We use var
for employeeName
, where the type is clear. Conversely, we caution against the excessive use of var
when calling the GetSomething()
method.
Utilize the Null-Conditional Operator
Introduced in C# 6.0, the null-conditional operator (?.
) provides an elegant solution for safe navigation through object hierarchies. It allows us to access properties and methods without worrying about null
references, enhancing code robustness.
Let’s see how to use the null-conditional operator:
var city = person?.Address?.City;
We use the null-conditional operator to access properties like City
without fear of null
references, bolstering code robustness.
Code Documentation
Clear and comprehensive documentation is the bridge that connects developers with our code. Utilize XML comments to provide descriptions for classes, methods, parameters, and return values. Well-documented code not only aids current developers but also simplifies onboarding for new team members.
Let’s add the XML comments to the method:
/// <summary> /// Calculates the area of a rectangle. /// </summary> /// <param name="length">The length of the rectangle.</param> /// <param name="width">The width of the rectangle.</param> /// <returns>The calculated area.</returns> public int CalculateArea(int length, int width) { return length * width; }
Here, we use XML comments to offer a structured way to describe classes, methods, and parameters.
Unit Testing
Unit testing is the foundation of code reliability. By creating test cases that validate the behavior of our code, we ensure that it works as intended and remains robust across changes. We use testing frameworks like MSTest, NUnit, or xUnit to automate and streamline the testing process.
Let’s see a simple example for writing a unit test case:
[TestClass] public class MathTests { [TestMethod] public void CalculateArea_ShouldReturnCorrectValue() { var math = new MathHelper(); int result = math.CalculateArea(5, 4); Assert.AreEqual(20, result); } }
Here, we create a test class using MSTest attributes to validate the CalculateArea()
method’s correctness. By automating tests with frameworks like MSTest, NUnit, or xUnit, we establish a systematic approach to validate code functionality. We test the MathHelper
class, confirming that the calculated area aligns with the expected value.
Never Trust the User’s Input
User input is the gateway for potential vulnerabilities. We should always validate and sanitize user inputs to prevent security breaches like SQL injection or cross-site scripting.
In addition, we can use validation libraries or built-in mechanisms to ensure that the data our application receives is safe and clean.
Let’s take a look at an example to validate the user input:
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { Console.WriteLine("Username or Password is invalid."); }
We validate the user input using the string.IsNullOrWhiteSpace()
method, ensuring that the username
and password
are not null
, empty, or contain only whitespace.
Performance Enhancements
We don’t settle for code that merely functions correctly; we strive for code that executes swiftly and conveys its intent with precision.
We explore techniques that enable us to achieve speed and clarity in our C# applications using best practices. From leveraging short-circuit evaluation with logical AND (&&
) and logical OR (||
) operators, to use of using
statement for the proper disposal of objects, we navigate the landscape of efficiency-driven coding.
Additionally, we delve into the decision-making process between value and reference types, understanding that choice lies at the heart of memory management and application performance.
Use && and || for Better Performance
Combining multiple conditions, we use the logical AND (&&
) and logical OR (||
) operators for better performance. These operators utilize short-circuit evaluation, meaning they cease evaluation as soon as they determine the final result.
Let’s see an example:
if (input.Length > 0 && input.StartsWith("prefix")) { } if (role == "admin" || role == "supervisor") { }
We use &&
to efficiently check if the input
has a length greater than zero and starts with a “prefix” and in the next case we employ ||
to determine if the role
is either “admin” or “supervisor.”
Handle Disposal With “using”
In C#, some objects need explicit cleanup, such as file streams or database connections. We use the using
statement to ensure that these resources are disposed of properly, even if an exception occurs. This practice not only prevents memory leaks but also promotes resource-efficient coding.
Let’s create a using
block to dispose of the fileStream
object:
using (var fileStream = new FileStream("file.txt", FileMode.Open)) { }
Here, we encapsulate the FileStream
within a using
block, enabling automatic disposal once the block is exited.
Choose Between Value Types and Reference Types
Value and reference types lie at the core of C# memory management. The stack stores value types, rendering them efficient for small data, while the heap stores reference types, making them suitable for more complex data structures. Understanding the distinction is essential for optimizing memory usage and ensuring our application’s performance.
Let’s see an example to choose between the value and reference type:
int age = 30; string name = "John";
Here, we make informed choices based on context, employing value types like int
for efficient stack storage of small data and using reference types like string
for managing more intricate data structures stored on the heap.
Advanced Techniques
As developers, we push the boundaries of conventionality and delve into innovative methodologies that elevate our coding prowess.
This section takes us beyond the fundamentals by exploring C# best practices techniques that enhance our coding prowess. From harnessing the efficiency of object initializers to creating loosely coupled classes for improved modularity and embracing dependency injection for enhanced flexibility, we unlock a comprehensive toolkit of advanced strategies.
Use Object Initializers
We can create and configure objects with object initializers in a single, concise expression. This technique not only enhances the readability of our code but also reduces the clutter of repetitive assignments.
Object initializers leverage curly braces to set the properties of an object during its creation succinctly. This technique is beneficial when we are dealing with immutable objects or classes with a multitude of properties.
Let’s see an example of the wrong and the correct way to initialize an object:
var person = new Person(); person.FirstName = "John"; person.LastName = "Doe"; person.Age = 30; var person = new Person { FirstName = "John", LastName = "Doe", Age = 30 };
In the initial approach, we follow the traditional method, which requires us to write multiple lines of code to assign values to the properties of the Person
object. Contrastingly, we adopt an object initializer in the following part, leading to more streamlined and efficient code.
Write Loosely Coupled Classes
Loose coupling is a design principle that promotes flexibility and maintainability. It entails minimizing dependencies between classes, allowing them to function independently.
By using interfaces, dependency injection, and design principles like the Dependency Inversion Principle, we create easily extensible code and are less prone to cascading changes.
Let’s declare an interface to achieve a loosely coupled class:
public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
We create an ILogger
interface that defines a Log()
method. Then, we define a class ConsoleLogger
that implements this interface, enabling us to log messages via the Log()
method.
Employ Dependency Injection
Dependency injection (DI) is a technique that promotes loose coupling and enhances testability.
We enable easier testing and modular code by injecting dependencies into a class instead of instantiating them. DI frameworks like Autofac or Microsoft’s built-in services provide powerful tools to manage dependencies.
For example, let’s inject the dependency in the constructor:
public class OrderService { private readonly IOrderRepository _repository; public OrderService(IOrderRepository repository) { _repository = repository; } }
We define an OrderService
class that accepts an instance of the IOrderRepository
interface through its constructor.
Conclusion
In this article, our primary objective has been to introduce the best practices of C#, which can prove invaluable in our coding endeavors. Keeping these practices in our toolkit can significantly enhance our coding journey.
We need to understand that these best practices represent just the tip of the iceberg, as there are more best practices in c# that help us to ensure code quality.
Remember that best practices may evolve, so stay engaged with the C# community to stay up-to-date with the latest recommendations.
If you have something to add to the list, we invite you to contribute by sharing it in the comment section.
var person = new Person();
person.FirstName = “John”;
person.LastName = “Doe”;
yes it’s a bit ugly but very easy to debug. Maybe I don’t know something, but duplicating variables for all fields will look worse. And the error stack in the log will look as it is a one line, instead of indicating which line variable has an error in an ugly method. How do you solve it?
Hey Oleksandr, what do you mean by “duplicating variables”?
Object initializers are used to initialize objects. If there’s a complex logic involved, you probably want to put your breakpoints somewhere else.
Some example, if there is any null or error character, there will be an error with no useful information. And I always create it as normal and later changes for production.
but I understood you, thank you)
Very good article
I think there can be a confusion here for some people:
“Another advantage of using the
Any()
method is its behavior withnull
collections. If we call theAny()
method on a null collection, it returnsfalse
, eliminating the need for additionalnull
checks.”Maybe a better wording would be empty collections instead of null collections.
Also related to single responsibility principle, there is no way to eliminate the ProcessOrder method entirely. Somewhere for sure there will be a method which will call other methods to execute the workflow you need.
Other than that I really enjoy your articles. I found them very insteresting and useful. Please keep them coming.
Hi Boghand.
Thanks a lot for the suggestions. The first one is already implemented – yes, it sounds a lot better and correct that way.
Also regarding SRP, well in the end we never manage to enforce 100% implementation of any architectural pattern, but we strive to implement and enforce as much as we can to make our code maintainable. I think this is the case here. You are correct that eventually there will be a method to call other methods, but this shouldn’t prevent us from trying to implement SRP as much as we can.
Finally, thank you very much for the kind words. We will give our best.