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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

If you’d like to learn the difference between the 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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!