.NET C# clean code and best practice Part-2

Abhishek Malaviya
8 min readSep 8, 2023

--

In this discussion, we’ll delve into a selection of recommended clean code and best practices related to .NET C#, aiming to enhance the meaningfulness and appeal of our coding endeavors.

Please read the following link: .NET C# clean code and best practice Part-1

https://medium.com/@mabhishekit/net-c-clean-code-and-best-practice-part-1-dec7c2f66122

Avoid creating overly long methods in your code.

Avoid creating overly long methods in your code. Instead, aim for shorter, more focused methods. Lengthy methods can make your code harder to read, maintain, and debug. They can also lead to code duplication and make it challenging to test and reuse portions of your code.

Breaking down lengthy methods into smaller, more focused child methods is a fundamental principle in software design, often referred to as “method decomposition” or “method extraction.” This practice enhances code readability, maintainability, and testability. Here’s how you can achieve this in C#:

Suppose you have a lengthy method like this:

public void SaveData(Data data)
{
// Step 1: Data validation
// ...

// Step 2: Data mapping from thrid party system
// ...

// Step 3: Data persistence logic
// ...

// Step 4: Logging
// ...

// Step 5: Notification
// ...
}

You can split it into smaller methods based on use cases:

public void SaveData(Data data)
{
if (!ValidateData(data))
{
return;
}

MappingData(data);
PersistData(data);
LogInfo(data);
Notification(data);
}

private bool ValidateData(Data data)
{
// Data validation logic
// Return true if data is valid, otherwise false
}

private void MappingData(Data data)
{
// Mapping data - integration with thrid-party API
}

private void PersistData(Data data)
{
// Data persistence logic
}

private void LogInfo(Data data)
{
// Logging logic
}

private void Notification(Data data)
{
// Notification logic
}

By breaking the code into smaller methods, each method has a specific responsibility, making the code easier to understand, test, and maintain. It also allows for better code reuse, as you can use individual methods in other parts of your application if needed.

Remember to choose descriptive method names that convey their purpose. Additionally, ensure that each method is cohesive, meaning it should perform a single, well-defined task. If a method becomes too long or complex, consider breaking it down further into smaller methods until each one is concise and focused.

Here are some benefits of using shorter methods:

Improved Readability: Shorter methods are easier to read and understand, making it simpler for you and other developers to grasp the logic and purpose of each method.

Easier Maintenance: Smaller methods are generally easier to maintain because they have a single responsibility. When you need to make changes or fix bugs, you can focus on one specific task within the method.

Enhanced Reusability: Shorter methods are more likely to be reusable in different parts of your codebase. They promote the principle of “Don’t Repeat Yourself” (DRY).

Better Testability: Smaller methods are easier to test because they have a well-defined and limited scope. Unit testing becomes more straightforward.

Reduced Complexity: Breaking down complex tasks into smaller, manageable methods simplifies the development process and reduces cognitive load.

Improved Collaboration: When working in a team, smaller methods are easier for team members to review and collaborate on. It also allows for parallel development since different team members can work on different methods concurrently.

To adhere to this best practice:

  • Single Responsibility Principle (SRP): Ensure that each method has a single, clear responsibility. If a method does too much, consider breaking it down into smaller methods.
  • Meaningful Naming: Choose descriptive and meaningful names for your methods to convey their purpose and functionality clearly.
  • Comments and Documentation: If necessary, use comments or documentation to explain complex logic or provide context for other developers.

Remember that there is no strict rule for the maximum length of a method, as it can vary depending on the programming language, coding style, and specific project requirements. However, as a general guideline, aim to keep your methods concise and focused on a specific task or responsibility. If a method becomes excessively long or complex, it’s a sign that it may be time to refactor it into smaller, more manageable parts.

Don’t use too many input parameters

Using a single entity or object with properties to encapsulate multiple input parameters is a good practice in C# programming. This approach is known as “parameter object” and it has several benefits:

Improved Code Readability: When you pass a single object with named properties, it’s easier to understand the purpose of the method or function, as the parameters are self-documenting.

Maintainability: If you need to add or remove parameters in the future, you can do so by updating the entity object rather than modifying every method that uses those parameters.

Type Safety: You can leverage the strong typing system of C# to ensure that the correct types of data are passed to your methods.

Here’s an example of how to use an entity to encapsulate input parameters:

public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfJoining { get; set; }
public decimal Salary { get; set; }
public long DepartmentId { get; set; }
public Address Address { get; set; }
}

public class EmployeeService
{
public void AddEmployee(Employee employee)
{
// Add employee to the database
// ...
}
....
....
}

In this example, the Employee class acts as a container for employee-related information. Instead of passing individual parameters like string firstName, string lastName, etc., you pass an Employee object. This approach makes your code more maintainable and readable, especially as the number of parameters increases.

Use Yield operator for lazily load sequence of items

The yield keyword in C# is used in iterator methods to create an iterator. It allows you to create a sequence of values lazily, which can be iterated over using a foreach loop or LINQ operations without the need to generate and store all the values in memory at once. This is particularly useful when dealing with large datasets or when you want to generate values on-the-fly.

Here’s an example of how the yield operator can be used:

using System;
using System.Collections.Generic;

public class Program
{
public static void Main()
{
foreach (var number in LoadItems(1000))
{
Console.WriteLine(number);
}
}

public static IEnumerable<int> LoadItems(int count)
{
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
}

The key benefit of using yield in this scenario is that the LoadItems method doesn't generate all the items upfront and store them in memory. Instead, it generates and yields one item at a time as requested by the foreach loop, making it memory-efficient for large sequences.

LINQ - one dot per line

The guideline to use one dot per line when working with LINQ (Language Integrated Query) is a matter of coding style and readability. It suggests breaking down LINQ operations into multiple lines, with one dot (period) per line, to improve code readability and maintainability. This practice can make complex LINQ queries easier to follow and debug.

Here’s an example of LINQ code with one dot per line:

Customer customer = dbContext.Customers
.Where(c => c.Age > 18)
.OrderBy(c => c.LastName)
.Select(c => new
{
FullName = $"{c.FirstName} {c.LastName}",
c.Email
});

Unmanaged resource handle by Using block

In C#, you can manage unmanaged resources (such as file handles, database connections, or COM objects) by using the using statement in conjunction with objects that implement the IDisposable interface. This ensures that the unmanaged resources are properly cleaned up when they are no longer needed, even if an exception is thrown. Here's how you can use the using block to handle unmanaged resources:

using System;
using System.IO;

public class Program
{
public static void Main()
{
// Example using a FileStream (unmanaged resource)
// The file stream will be automatically closed and disposed when the using block is exited
using (var fileStream = new FileStream("test.txt", FileMode.Open))
{
// Work with the file stream here
// ...
}
}
}

Use constant or enum instead of hard-coded values

It helps improve code readability, maintainability and reduces the risk of introducing errors due to typos in string literals. Here’s how you can use constants and enums to replace magic values:

public class Constants
{
public const string FirstName = "FirstName";
public const string LastName = "LastName";
}
// Example
string firstName = item[Constants.FirstName];
string lastName = item[Constants.LastName];

Enums provide type safety and can be especially useful when dealing with a fixed set of values.

When using constants or enums, make sure to choose descriptive names to improve code readability. Additionally, consider grouping related constants or enums into classes or namespaces to maintain a clean and organized codebase.

enum Risk 
{
Low,
Medium,
High
}

Don’t use toLower() toUpper() methods for string comparison

These methods incur performance penalty. Better way you can use string.Equals() to avoid null reference exception and to avoid case sensitive comparison

//Use string.Equals to avoid null reference exception and to avoid case sensitive comparison
if(string.Equals("Title", "title", StringComparison.OrdinalIgnoreCase))
{
.....
}

Since strings are immutable, performing numerous operations on them can result in a significant performance and memory overhead.

If we have limited number of operations 2 to 4 better go with string.Format() or String Interpolation ($”{…}”). It’ll take lesser memory consumption.

//string.Format emaple
string s = string.Format("Hello {0} {1}", "Abhishek", "Kumar");

//string interpolation example
string s1 = $"Hello {"Abhishek"} {"Kumar"}";

For more extensive concatenations or operations within loops, StringBuilder is the preferred choice because it minimizes memory overhead and provides better performance.

Using System.Text;

StringBuilder sbTemplate = new StringBuilder();
sbTemplate.Append("Some modification");
..
..
..
sbTemplate.AppendLine("Some modification");

Always pass cancellation token to Task object

It’s a good practice to pass a cancellation token when working with Task objects, especially for asynchronous operations that may need to be canceled. The cancellation token allows you to gracefully cancel or abort a running task when necessary. This is particularly important in scenarios where long-running or potentially blocking operations are involved. Here's an example of how to use a cancellation token with a Task:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

namespace ConsoleApplication1
{
public class SampleClass
{
public void ShowMessage()
{
//Cancellation token example

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

Task.Run(() =>
{
while (!token.IsCancellationRequested)
{

Write("*");
Thread.Sleep(1000);
}
token.ThrowIfCancellationRequested();
}, token);


WriteLine("Press enter to stop the task");
ReadLine();
tokenSource.Cancel();
WriteLine("Press enter to end the application");
ReadLine();
}
}

class Program
{
private static string result;
static void Main(string[] args)
{
SampleClass sampleClass = new SampleClass();
sampleClass.ShowMessage();
}
}

Here once you press any key, it’ll trigger tokenSource.Cancel() which will stop further processing of running task.

By using a cancellation token in this way, you can effectively manage the cancellation of tasks and ensure that resources are cleaned up properly when tasks are canceled.

Output

Use Task.WhenAll If asynchronously waiting for multiple tasks

Task.WhenAll is a method in C# used for asynchronously waiting for multiple tasks to complete concurrently. It returns a new task that represents the completion of all the provided tasks. This can be useful when you have multiple independent asynchronous operations that you want to execute concurrently and wait for all of them to finish before proceeding.

Both Task.WhenAll in C# and Promise.all() in JavaScript are essential for concurrent execution of multiple asynchronous tasks and waiting for all of them to complete before moving on to the next steps in your code.

Here’s a basic example of how to use Task.WhenAll:

using System;
using System.Threading.Tasks;

public class Program
{
public static async Task Main()
{
Task<int> task1 = SampleTask(10);
Task<int> task2 = SampleTask(20);
Task<int> task3 = SampleTask(30);

// Use Task.WhenAll to wait for all tasks to complete
await Task.WhenAll(task1, task2, task3);

Console.WriteLine("All tasks are completed.");
}

public static async Task<int> SampleTask(int id)
{
// Simulated asynchronous work
await Task.Delay(TimeSpan.FromSeconds(id));
Console.WriteLine($"Task {id} completed.");
return id;
}
}

Task.WhenAll allows you to avoid blocking the main thread and efficiently run multiple tasks in parallel, waiting for all of them to finish before continuing with your program's execution. It's especially helpful in scenarios where you want to maximize concurrency and parallelism in your asynchronous code.

Continuous learning and practice are essential for becoming a proficient programmer.

Thanks for reading.

--

--

No responses yet