This document presents the set of C# coding standards we maintain across all our projects at AESD Lab,. Letsโ dive straight in.
We will use several designations:
๐/๐ Good practice / Bad practice
โ Explainer
๐ Pay attention
๐งฐ Code analysis tools support (RS - ReSharper, VS - Visual Studio (Roslyn powered))
Letโs start with a note on code formatting:
- Do not overly concentrate on it.
- Do not make it the focus of criticism during reviews with your teammates. It is always better to concentrate on more important things first like feature design, code reuse, potential bugs, testing and only then on formatting.
- Do not auto-format whole files which were created by others. If you do so this will make the version control file history unreadable when you commit to Git.
- Do not waste time looking for the โidealโ tool for auto-formatting. They vary greatly in terms of strictness and style. This is ultimately why we do not designate a specific formatting style. The goal should always be to keep code readable.
Learning advice: read books and listen to the opinions of others, but always exercise your own judgement. All ideas on the subject are the product of an individual's own experience and beliefs. Try out the theory in practice. Don't be afraid to experiment, but make sure your teammates understand your approach before finding it production :)
Declaration
Types
๐ Do use var when the type is evident from right side of the assignment
๐ Do use explicit typing for inline initializers
int counter = 0;
string name = "John";
var pastaGood = new Pasta();
Pasta pastaBad = new Pasta();
Pasta pastaFromMethod = CreatePasta();
Pasta pastaEvident = Create();
for (int i = 0; i < 100; i++) { i--; }
for (var i = 0; i < 100; i++) { i--; }
foreach (Pasta pasta in pastas) { pasta.Do(); }
foreach (var pasta in pastas) { pasta.Do(); }
Pasta[] pastasInline = { new Pasta(), new Pasta() };
Pasta[] pastasOldStyle = new[] {new Pasta(), new Pasta()};
string[] namesOldStyle = new[] {"a", "b"};
โ why: Use explicit types for collection enumerators. The collection base type will then be readable at a glance without the need for additional IDE help
โ why: Using var declarations whenever it is easy to see the type without additional IDE help. Avoids redundant duplication
๐งฐ VS, RS, Roslynator
๐ Do prefix interfaces with I
public interface IPastaFactory
{
public Pasta CreatePasta();
}
public class MagicRestaurant : FoodFactoryBase, IPastaFactory, I...
{
Pasta IPastaFactory.CreatePasta() { ... }
โ why: It becomes easier to distinguish an interface from a base class in derived class declarations
โ why: It becomes easier to distinguish explicit interface implementation from regular methods
๐ Do correlate namespace hierarchy with folders hierarchy
// RestaurantAdvancer.dll: ~/RestaurantAdvancer/Meal/Pasta.cs
namespace RestaurantAdvancer.Meal { ... }
โ why: This practice maintains a uniform and predictable project structure
โ why: Encourages design considerations from the beginning of implementation
โ why: Speeds up refactoring when project structure needs to be changed with compiler support
๐งฐ RS, Roslynator
Class Member Arrangement
๐ Do arrange members according to the type responsibility and lifecycle
The objective here is to establish definitive sorting rules so that we know precisely where to place each and every declaration inside a class. The rules presented here are, in fact, a common standard we follow within any OO (Object Oriented) language.
Note that automatic class member arrangement is only fully supported by ReSharper. However it is still manageable by hand when creating and editing new classes
๐งฐ RS
Here is the pattern:
public class MagicRestaurant : FoodFactoryBase, IPastaFactory, I...
{
-
Non-private nested enums and delegates:
Do declare in PascalCase order by kind then alphabetically
โ why: Enum type declarations are easy to lose track of the middle of the file
โ why: IDE navigation from outside will lead us to the beginning of the filepublic enum KitchenMode { ... } public delegate int StuffMotivationStrategy();
-
Non-private constants and static properties:
Do declare in PascalCase and order alphabetically
โ why: Visibly separates statics and constants from object state
โ why: IDE navigation from outside will lead us close to the beginning of the file
๐ Avoid exposing public static fields (variables), especially in a class library, as this practice can limit the ability to make changes. See this example which demonstrates the preferred alternatives to public static variables// ๐ Empty Line public static MagicRestaurant Default { get; } = new MagicRestaurant(); public const string RestaurantName = "Magic";
-
Private constants and static fields:
Do declare in PascalCase and order alphabetically
โ why: Visibly separates statics and constants from object state
โ why: IDE navigation from inside will lead us close to the beginning of the file
๐ The example below demonstrates our preferred declaration style of private constants and static fields. You will note that static auto-properties are avoided completely. We prefer to use static methods instead of computed static properties so as to easier distinguish data members from behavior. Similarly, we prefer readonly fields instead of static auto-properties to easier distinguish instance members from static membersprivate const string DefaultPastaRecipeName = "Spaghetti"; private static readonly string[] NoIngredients = new string[0];
-
Public auto-properties:
Do declare in PascalCase and order alphabetically
โ why: They are shorthand to properties (instance data) declared by the compiler in a private anonymous backing field
โ why: Declared prior to the rest of instance (non-static) data for better IDE navigation from outside
๐ For entity models where a large part of public data is exposed this group, we sort differently, always starting with entity's id and ending with optional parts and associationspublic string CurrentPastaRecipeName { get; private set; } public MagicRestaurantOptions Options { get; }
-
Private fields:
Do declare in _camelCase. Order readonly first then alphabetically
โ why: This is the remainder of object (non-static) data
โ why: Readonly members are separated to improve readability. Usually they reference aggregated or composed dependenciesprivate readonly List
_currentStuff; private readonly ILogger _logger; private int _createdPastaCount; private double _pastaPerHour; -
Constructors:
Do declare in PascalCase. Order by signature length
Do place constructors immediately after data members
โ why: To simplify understanding checks of invariants since the data members have been read
โ why: Before any behavior (methods) to keep close to the data declarations// ๐ Empty Line private MagicRestaurant() { ... } public MagicRestaurant(MagicRestaurantOptions options) { ... }
-
Non-private static methods:
Do declare in PascalCase. Order factory methods first then by usage then by name
โ why: Static factory methods should directly follow constructors but such sorting can't be automated. It is possible to disable auto-sorting in the group. A preferred solution specifically for factory methods is to contain them using a #region
โ why: Sorting "by usage" means that top level methods in the call hierarchy appear before lower level methods in the call-hierarchy
โ why: This group precedes non-private, non-static members in subsequent groups. It is preferable to keep this group as short as possible to quickly arrive at those methods that are the main responsibility of the class. As a coding style we prefer extracting the body (logic) of methods in this group, especially if the body is long, to private methods in the private group (see Group 10)
โ why: Sorting "by usage" is a manual process, it can not be automated. However we can simplify the process by extracting a common private method down to the private group (see Group 10)// ๐ Empty Line public static MagicRestaurant Create(KitchenMode mode) { ... } internal static string[] GetRequiredStuff(KitchenMode mode) { ... }
-
Overrides and Interface Implementations:
Do place in the same order as the order of inherited types
โ why: Can be automatically wrapped with regions
โ why: Usually automatically sorted with original order in the base type when generated// ๐ Empty Line #region FoodFactoryBase protected override string[] GetSuppliedIngredients() { ... } #endregion #region IPastaFactory Pasta IPastaFactory.CreatePasta() { ... } #endregion
-
Non-private Events, Properties and Methods:
Do declare in PascalCase. Order by kind then abstract first then by usage then by name
โ why: Order by โkindโ means: events โ properties โ methods
โ why: Sorting "by usage" means that top level methods in the call hierarchy appear before lower level methods in the call-hierarchy
โ why: Sorting "by usage" can not be automated, but, as stated previously, manual sorting can be avoided or simplified by extracting common private methods down to the private group (see Group 10)
โ why: If some members in this group were extracted from an interface then move the whole implementation to the previous group (Group 8) and implement the interface explicitly there// ๐ Empty Line public event Action
OnMagicCooked; protected abstract string RequiredMagic { get; } protected virtual string AdditionalMagic { get { ... } } protected Pasta CreatePasta() { ... } -
Private methods (both static and instance):
Do declare in PascalCase. Order by usage
โ why: There is no need to apply name ordering as this group will be the most intensively refactored. Names can change often
โ why: There is no ordering by kind as only private methods remain
โ why: Sorting "by usage" means that top level methods in the call hierarchy appear before lower level methods in the call-hierarchy
โ why: Sorting "by usage" can not be automated, however maintaining the order manually is worth the effort - it allows sequential reading of algorithms in an intuitive order from general to specific. This promotes the reader's understanding of the logic
โ why: If some part of members from this group have been extracted to interface then move the whole implementation to the corresponding group (Group 8) and implement interface explicitly there
โ why: The example below shows "by usage" sorting// ๐ Empty Line private void NotUsedInThisGroup() { ... } private void ThisIsUsedByNotUsedInThisGroup() { ... }
-
Private nested enums, delegates, classes:
Do declare in PascalCase. Order by kind then alphabetically
โ why: Kind order: enums โ delegates โ classes
๐ The members arrangement rules are the same for nested classes
๐ This group contains only no non-private classes. Note that directly accessible public nested classes/structs should always be extracted into an independent (compilation) unit. This is done, not least, to avoid IDE navigation to the end of file each time// ๐ Empty Line private struct CookStepDescription { ... }
}
Naming Conventions: Anti-patterns
๐ Do not adopt any of the following anti-patterns when naming identifier in type/class declarations. Specifically these refer to redundant prefixes, suffixes and abbreviations
public enum FoodTypeEnum { ... }
public enum EFoodType { ... }
public struct OptionsStruct { ... }
public struct SOptions { ... }
public delegate void ConfiguratorDelegate(...);
public delegate void ConfiguratorDlg(...);
public delegate void DConfigurator(...);
public class MagicRestaurantClass { ... }
public class MagicRestaurantCls { ... }
public class CMagicRestaurant { ... }
โ why: Redundant means duplication of type information already evident in the member declaration e.g. duplication of the keyword class or enum
โ why: This style has a negative impact on code readability
โ why: Avoidance reduces count of affected files while refactoring
๐ Do not mark any variable declaration with redundant prefixes, suffixes or abbreviations indicating type of variable
int iCounter;
string strName;
โ why: Redundant means duplication of type information already evident in the variable declaration
โ why: This style reduces readability of code by unnecessary inflation of method bodies
โ why: Variable type information is easily discoverable via the IDE tooltips
๐ Do not prefix declarations with namespace abbreviation
// RestaurantAdvancer.dll
namespace RestaurantAdvancer.Meal
{
public class MealPasta { ... }
public class MPasta { ... }
}
โ why: Redundant for languages supporting scoped type naming
โ why: May lead misunderstanding of the purpose of the type
โ why: Reduces readability of code by unnecessary inflation of method bodies
โ why: Full type name is already discoverable via IDE tooltips
Layout
Spacing
๐ Do indent with 4 spaces and declare blocks with BSD (Berkeley Software Distribution) style of brace placement
๐ We prefer to follow this as the most common convention among existing projects. Furthermore this is a default configuration among our code analysis tools
โ why: In the example below, variable result is initialised using a variety of syntax styles. This is given to demonstrate spacing rules. In practice, the most appropriate initializer style will be advised by your chosen code analysis tool and applied consistently
internalยทstaticยทstring[]ยทGetRequiredStuff(KitchenMode mode)
{
ยทยทยทยทstring[]ยทresult;
ยทยทยทยทswitch (mode)
ยทยทยทยท{
ยทยทยทยทยทยทยทยทcase KitchenMode.Evening:
ยทยทยทยทยทยทยทยท{
ยทยทยทยทยทยทยทยทยทยทยทยทresultยท=ยทnew[]ยท{ยท"Chef",ยท"cook",ยท"waiter",ยท"manager"ยท};
ยทยทยทยทยทยทยทยทยทยทยทยทbreak;
ยทยทยทยทยทยทยทยท}
ยทยทยทยทยทยทยทยทcase KitchenMode.ClosedForQuarantine:
ยทยทยทยทยทยทยทยท{
ยทยทยทยทยทยทยทยทยทยทยทยทresultยท=ยทnewยทstring[]ยท{ยท"watchman"ยท};
ยทยทยทยทยทยทยทยทยทยทยทยทbreak;
ยทยทยทยทยทยทยทยท}
ยทยทยทยทยทยทยทยทdefault:
ยทยทยทยทยทยทยทยท{
ยทยทยทยทยทยทยทยทยทยทยทยทresultยท=ยทnewยทstring[1];
ยทยทยทยทยทยทยทยทยทยทยทยทresult[0]ยท=ยท"manager";
ยทยทยทยทยทยทยทยทยทยทยทยทbreak;
ยทยทยทยทยทยทยทยท}
ยทยทยทยท}
ยทยทยทยทreturn result;
}
๐งฐ VS, RS, Rolsynator
Wrapping
๐ Do extend classic code wide limitation from 80 to 120-160 characters per line
public LongGenericResult MayLeadToOftenWrapping(...)
{
GenericResult result = PrivateMethodWithLongName(...)
bool wellDefinedMeaningOfPredicate = _some && _complex && _predicate;
if (wellDefinedMeaningOfPredicate)
{
// use contextual variable names
bool longVariableNameCorrelatingWithMeaningOfThePredicate = ...
// or extract logic to a separate method with separate context
MethodNameCorrelatingWithMeaningOfThePredicate();
}
}
private void MethodNameCorrelatingWithMeaningOfThePredicate()
{
bool shortVariableName = ...
}
โ why: It is important to give identifiers meaningful, contextual names. Do not abbreviate identifier names to the point where meaning/context becomes hard to determine. This is especially true for higher level languages where meaningful identifiers and signatures are essential. A consequence of longer identifiers is the effect on line wrapping in the editor
โ why: Extending the line width limit allows you to reduce the number of times you need to apply line wrapping
๐ There is also the rule to limit wrapping by controlling logic structure: keep top level identifiers clean from the context of a current method because they are close to the method name. Start conditional blocks with well defined names and keep inner variable names contextual as well. At the point you see the inner name is too long and/or repeated several times - extract a new method and cut the contextual part of variable to the new method name. Repeat recursively and lazily
โ ๐ In certain editors which do not enforce code wide limitation, you will find a feature which draws a vertical line (or lines) at a given character offset from left margin e.g. 120, 160 etc
๐งฐ VS, RS
๐ Do choose suitable and effective wrapping style
public LongGenericResult MayLeadToOftenWrapping(
int argument1, int argument2, int argument3)
{
โ why: The line break before the first formal argument is effective for a short argument list which fits onto separate single line
๐งฐ RS, Roslynator
public LongGenericResult MayLeadToOftenWrapping(
LongGenericResult argument1,
DelegateWithMeaningfulName argument2,
ImplementedAlgorithmOptions argument3 = default)
{
โ why: Line break after each argument
โ why: Good for long argument declarations
โ why: Good when signature contains a generic argument
โ why: Good when signature contains a default argument
๐งฐ RS, Roslynator
var result = new PocoModel
{
Member1 = "value1",
Member2 = "value"
};
โ why: When parameterless constructor is combined with member assignment or collection initialization, as in the above example, it is recommended to isolate the variable assignment from members assignment with line break separators
๐งฐ RS, Roslynator
Empty Lines
๐ Do put empty lines around blocks and outlying variables
public int AddKetchup(Ketchup ketchup, int amount)
{
if (ketchup == null)
throw new ArgumentNullException();
if (amount < 0)
throw new IndexOutOfRangeException();
if (amount = 0)
return 0;
string sharedBlockCalculatedValue = null;
string usedInSingleBlockValue = ketchup.Boil(amount / 2);
if (usedInSingleBlockValue != null)
{
string notification = $"Pay attention {usedInSingleBlockValue}";
sharedBlockCalculatedValue = SendChiefNotification(notification);
}
int result;
if (sharedBlockCalculatedValue != null)
{
result = LimitKetchupNextTime(amount, sharedBlockCalculatedValue);
}
else
{
result = RequestMoreKetchupNextTime(amount);
}
return result;
}
โ why: Series of blockless assertions are not separated due to uniformity and compactness
โ why: Regular code following the blockless condition or loop constructs is separated
โ why: Variables that are initialized in the next block, but used in the code after it, are separated
โ why: Method result variables are not separated due to return statement compactness
๐ Do declare variables as close as possible to the point of usage
โ why: Readability of the code will degrade if attention is distracted by variables introduced earlier than necessary
โ why: To simplify refactoring when method extraction is considered
โ why: To avoid accidental or redundant variable usage after new conditional logic has been added to the method
๐ Do not leave pointless empty lines
public void AddKetchup(Ketchup ketchup, int amount)
{
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
if (ketchup == null)
throw new ArgumentNullException();
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
if (amount < 0)
throw new IndexOutOfRangeException();
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
if (amount = 0)
return;
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
string sharedBlockCalculatedValue = null;
string usedInSingleBlockValue = ketchup.Boil(amount / 2);
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
if (usedInSingleBlockValue != null)
{
string notification = $"Pay attention {usedInSingleBlockValue}";
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
sharedBlockCalculatedValue = SendChiefNotification(notification);
}
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
if (sharedBlockCalculatedValue != null)
{
LimitKetchupNextTime(amount, sharedBlockCalculatedValue);
}
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
else
{
RequestMoreKetchupNextTime(amount);
}
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
}
โ why: Forces the reader to scroll more
โ why: Related statements and blocks becomes undistinguishable
โ why: Untidy and unprofessional. Nobody wants to be remembered for this!
Commenting
๐ Do ensure committed comments are sufficiently meaningful to others. For example the context of a TODO should be clear
โ why: Example: you have a task to add a feature to a component or make it more scalable. You know how to do it but currently you don't have time. Use comments appropriately to annotate your design or solution. This will assist you or your teammate's efforts when time is available to implement the changes. The easiest option would be to leave a short description right in the comment. Better still would be to elaborate a task in the project management system. In both cases don't forget to make your TODO distinguishable from the others
// TODO [Pasta scaling] parallel macaroni processing
// TODO [Pasta scaling DEV-007] parallel macaroni processing
public override Task Cook(...) { ... }
โ why: Eventually you will search for your TODO
โ why: Eventually your teammate will search for his or your TODOs
๐ Do use XML documentation comments
/// <summary>
/// Represents generic food characteristics
/// </summary>
public class Food
{
/// <summary>
/// Translates the food instance into cooked state
/// </summary>
/// <param name="tools">Cooking tools</param>
/// <exception cref="CookFoodException">Food cooking issue</exception>
public abstract Task Cook(string[] tools) { ... }
}
/// <summary>
/// Represents pasta characteristics
/// </summary>
/// <seealso cref="Macaroni">
public class Pasta : Food
{
/// <inheritdoc />
public override Task Cook() { ... }
}
โ why: Structured, inheritable, convertible, interactive
โ why: IDE integration to XML comments accelerates learning of the documented feature
๐งฐ RS, Roslynator
๐ Do inherit documentation for inherited members
/// <summary>
/// Represents pasta characteristics
/// </summary>
/// <seealso cref="Macaroni">
public class Pasta : Food
{
/// <inheritdoc />
/// <exception cref="PastaException">Pasta cooking issue</exception>
public override Task Cook() { ... }
}
โ why: Correlates with known Liskov principle (ref SOLID)
โ why:
allows propagation of base class comments to direct usage of derived class
๐ Avoid adding additional documentation elements to inherited members, which would otherwise indicate a break with the Liskov principle
๐ Do limit documentation of data members to the these cases
(a) data models
(b) abstract declarations
/// <summary>
/// Describe the entity meaning
/// </summary>
public class PocoModel
{
/// <summary>
/// Public id of chosen type
/// </summary>
public int Id { get; set; }
/// <summary>
/// Member1 describes the entity attribute or state
/// </summary>
public string Member1 { get; set; }
/// <summary>
/// Member2 as Member1 both goes to your generated docs
/// </summary>
public string Member2 { get; set; }
}
โ why: Follow these rules for a visually clean and tidy class implementation
โ why: For API (Application Programming Interface) building it will help to generate documentation
โ why: Consider that excessive commenting might be a sign that the implementation is too complex or not at the right level of abstraction. Refactoring, decomposition can address this and reduce commenting
๐งฐ VS
๐ Do not use inline comments which make the code less readable
For simple logic there is no need for commenting. For complex logic there are always better ways than commenting to simplify its understanding. At a minimum you can extract the complex part into a separate method and document that method properly. Sometimes the Strategy Pattern can be also useful for complex decision hierarchies and also for testing. The definition of the strategy should be self-explanatory enough or at least offer a cleaner place to put comments.
public static MagicRestaurant Create(KitchenMode mode)
{
string[] requiredStuff;
// select minimal required stuff to prepare the menu
{
// some complex and long logic
}
var options = new MagicRestaurantOptions
{
RequiredStuff = requiredStuff;
};
return new MagicRestaurant(options);
}
public static MagicRestaurant CreateRefactored(KitchenMode mode)
{
var options = new MagicRestaurantOptions
{
RequiredStuff = GetRequiredStuff(mode);
};
return new MagicRestaurant(options);
}
/// <summary>
/// Select minimal required stuff to form up the menu
/// </summary>
internal static string[] GetRequiredStuff(KitchenMode mode) { ... }
โ why: Inline comments inflate method size
โ why: Inline comments interfere with code cleanliness and readability