Following you will find some characteristics for high quality C# code. Of course it is possible to define much more characteristics. So the following code quality features are an entry point to this topic only.
Define a standard constructor
If a class does not contain a constructor, the compiler will automatically include a standard constructor. Therefore it is possible to implement a class without any constructor. The disadvantage of this C# feature will become noticeable as soon as you have to write an own specific constructor for your class. In this case the compiler will no longer create a standard one. With such an update of your implementation, all existing applications which use your class will break during compilation or even on runtime of you exchange the assembly containing your class.
Therefore, to avoid such issues, you should always implement a standard constructor.
Single point of return
There exists a common best practice: Methods should have a single point of return. This will increase the readability of the method code.
I think this best practice is a good advice for standard cases, but it is often not true in error cases. If you have to do some checks for error cases and valid data, the method code may become very complex as it results in nested if-else-statements. So the “single point of return” advice may collide with the advice to avoid nested if-else-statements. Therefore I think these two practices should be combined into one.
To avoid nested if-else-statements you may create single if-statements at the beginning of you method to handle all error cases. This may be for example a check of the input parameters, the creation and validation of the data used in the method or a check if needed components or services are ready to use. Each single error case should be executed in an atomic check and if the check fails the method may immediately return.
The code for the error cases is followed by the code for the actual standard use case of the method. This standard use case should have a single point of return only.
So the “single point of return” advice is valid for a method, but it should be used for all atomic parts of the method. That means, each error use case and the standard use case should have their own single point of return.
Use properties instead of fields
In classes you may have private fields which can be accessed via public properties. Within the class you can access both of them. But which one should be used?
If you use properties with automatic generated fields, you don’t have a field variable at all and you have to use the property. So this case is easy to answer. But in all other cases it is also recommended to use the property for the following reasons.
You can easily refactor your code and create a base class if necessary. In this case you move the property and the field to the base class and it will no longer possible to get or set the field within the actual class. Or you may already have a base-child structure of classes and already use the properties instead of the field. In all of these cases you will access properties.
The above issues are already good reasons to use the property. But I think the main issue is: the property may contain additional code. This can be a simple logging, a data validation or even some logic like informing other program parts about the changed value. If the field is used directly, the additional code of the property is not executed which may result in unpredictable issues.
Some will argue against the property usage because a method call is much slower than a direct access of a field. This sounds true in the first moment. But if the property just sets or gets the field, the compiler will create an performant inline method.
Therefore in summary, within you class you should always use properties and you should not change fields directly.
Do not use var
Of course the “var” keyword is mandatory in some situations, for example if you want to declare an anonymous class. But in other cases where “var” is optional you can decide whether to use the concrete type or to use “var”.
I think there is only one situation where “var” will help to create more readable code: If you define a type with a complex type name and additional create the according object instance in the same line of code. For example the declaration and initialization of a dictionary may be written by using “var”:
var dict = new Dictionary();
If the complex type is returned by a method, you should not use “var” because in this case the developer cannot see the type of the variable. So for the above example the code will be look like:
Dictionary dict = CreateDictionary();
In summary the common advice is: do not use “var”. The only exception is, in case you create and initialize a complex type.
Reuse objects in loops
In .NET the garbage collector is responsible to find unused objects and free and clean up the memory. Therefore as software developer you can create object and don’t have to think about memory usage and execution speed of your code. But if you create a lot of not needed objects you may slow down your application as the garbage collector needs a lot time to do his job.
A common mistake is the repeated creation of objects within loops. Often these objects are needed only for a short period of time to do some task within the actual loop iteration. In such cases you can create one object outside of the loop and use the same object within the iterations of the loop. Maybe your object can provide a reset function to create an initial status or you have a function to set the properties for the initial status.
In summary, even if you normally don’t have to think about object lifetimes and memory management, you may help the garbage collector a little bit if you do not create not needed object. So, reuse objects in loops may be a first but important step.
A common mistake in C# is the lax use of events. As C# developers normally don’t have to think about garbage collection they will forget to unregister events. These hanging references to objects will prevent the garbage collector to delete these objects and free the reserved memory. Therefore you should always unregister events. In best case the event is unregistered if it is no longer needed, or in standard case it should be unregistered in the dispose function.