C#
This guide establishes a default approach to development in C# at Headspring that defines our standards and sets clear expectations for Engineers. It also serves as reference material to aid in streamlining new Engineer onboarding and helps everyone become more self-sufficient.
Code that has a predictable structure and follows guidelines enables developers to adjust to the purpose of code and not an unfamiliar, or distracting, convention. Established conventions promote readability and affirm expectations that developers will have on first inspection. Code becomes easier to navigate and simpler to understand when we follow established conventions and standards. Coding standards take into account that other developers will read and maintain the code in the future.
- Importance of Consistency
- DO default on new projects to conform to this guide
- DO conform to pre-existing code styles in a project, even when they differ from this guide
- AVOID mixing multiple coding styles in a single code base
- DO utilize tooling configuration files to control style and structure automatically across developer environments
- General C# Standards
- DO use var instead of explicit types where possible.
- DO NOT use an explicit this qualifier when referencing member fields and methods
- DO use aliases for built-in types instead of BCL class names
- CONSIDER using string interpolation instead of string.Format()
- DO use four spaces instead of two spaces or tabs per indentation level
- DO NOT have more than one sequential empty (whitespace) line
- DO remove unused or redundant using statements (or directives)
- DO place braces for multi-line statements on their own line
- DO NOT follow or precede opening or ending braces with a blank line
- Basic Layout Of Class Members
- Naming Conventions
- DO use PascalCasing for class names, method names, properties and namespaces
- DO use camelCasing for method arguments, local variables and field names
- DO NOT Use Hungarian Notation, prefixing data type abbreviations to variable or field names
- DO use capitalization to separate two different words in the name of an identifier
- AVOID Using abbreviations or shorter versions of words - as in GetWin for GetWindow
- DO use PascalCasing for abbreviations of three characters or more
- DO NOT use underscores to separate words
- CONSIDER creating a list of exclusions in a shared ReSharper dotsettings file to hold well-known domain abbreviations
- DO use noun or noun phrases to name a class
- AVOID using overly generic type names
- DO prefix interfaces with the letter ‘I’
- Visibility of Types and Members
- DRY Principle
- Use of Regions
- Self-Documenting Code
- DO use mindful naming and recognizable patterns when naming methods, variables, etc. to express intent
- CONSIDER providing any additional information associated with your code in the commit text, rather than a comment block
- DO NOT push TODO comments to main or dev branches
- AVOID the use of comments to explain poorly written code
- DO use comments when documentation is necessary or required by the client
- DO NOT comment out code
- Boy Scout Rule
- DO keep your changes in the scope of the feature or bug you are working on
- CONSIDER renaming variables, parameters, and methods
- CONSIDER fixing small bugs you discover
- CONSIDER refactoring large classes to increase readability
- CONSIDER potential merge or team conflicts for your changes
- CONSIDER creating “Refactor” or “Cleanup” tickets to track larger cleanup efforts you identify
- Composition vs Inheritance
- DO favor composition over inheritance
- DO use interfaces to define contracts between classes
- DO NOT use inheritance when the subclass is not a proper sub-type of the superclass
- DO NOT inherit across domain boundaries
- CONSIDER single inheritance for establishing conventions in a single layer of an application
- AVOID inheritance of more than three levels
Importance of Consistency
Many of our projects are not “greenfield.” We often take over existing, mature code bases or work alongside existing client engineering teams or even other consultant firms. It’s tempting to forge ahead with your own conventions, naming styles, etc. (many of which exist in this document) without considering the impact on the overall code base. However, the correct approach is to adjust your style to match what’s already established.
The code we write will be read by many other people for years to come, starting with our fellow developers and tech leads. Once we write software for a client, we hand it off and then leave it with them, with the goal that either they or any other team of developers could easily maintain it. If the code base is a mixed bag of style and structure, readers will find it difficult to parse, which makes it difficult to maintain. The goal is to have a code base that looks like it was written by one developer.
DO default on new projects to conform to this guide
DO conform to pre-existing code styles in a project, even when they differ from this guide
AVOID mixing multiple coding styles in a single code base
DO utilize tooling configuration files to control style and structure automatically across developer environments
Visual Studio uses .editorconfig
files and R# uses *.DotSettings
files to persist configurations that can be shared across environments and stored in source control
Headspring’s default ReSharper settings can be found in the following repository:
Resharper Configuration(internal repository)
General C# Standards
DO use var instead of explicit types where possible.
Exception: Needing an explicit sub-type, such as:
ReadOnlyCollection<People> people = GetPeople(); // returns List<People>()
DO NOT use an explicit this qualifier when referencing member fields and methods
// Bad
public void SampleMethod(string name)
{
this.Name = name;
this.AddToList(name);
}
// Good
public void SampleMethod(string name)
{
Name = name;
AddToList(name);
}
DO use aliases for built-in types instead of BCL class names
// Bad
String.IsNullOrEmpty(person.Name);
// Good
string.IsNullOrEmpty(person.Name);
CONSIDER using string interpolation instead of string.Format()
// Bad
var message = string.Format("You live in {0}, {1}.", address.City, address.State);
// Good
var message = $"You live in {address.City}, {address.State}.";
DO use four spaces instead of two spaces or tabs per indentation level
// Bad
public void DoSomething()
{
••Foo();
}
// Good
public void DoSomething()
{
••••Foo();
}
DO NOT have more than one sequential empty (whitespace) line
// Bad
public void DoSomething()
{
Foo();
Bar();
Baz();
}
// Good
public void DoSomething()
{
Foo();
Bar();
Baz();
}
DO remove unused or redundant using statements (or directives)
DO place braces for multi-line statements on their own line
// Bad
public void DoSomething() {
Foo();
}
// Good
public void DoSomething()
{
Foo();
}
DO NOT follow or precede opening or ending braces with a blank line
// Bad
public void DoSomething()
{
Foo();
Bar();
}
// Good
public void DoSomething()
{
Foo();
Bar();
}
Basic Layout Of Class Members
Code should be readable so developers with no background on a project can navigate a class without having to adjust to a unique convention. Ordering the members of a class provides an expected structure that allows developers to understand new code quickly and thoroughly.
The statements below apply to a green-field project - where we get to make decisions on new classes that we set up. Often times, we are called upon to help with an existing system. In those cases, we should follow the established patterns of the client.
DO write class members in the conventional order below, so that members are found more easily
Order of Members
Place members for all classes, interfaces and structs in this order:
Constant Fields Fields Constructors Finalizers (Destructors) Delegates Events Enums Interfaces (interface implementations) Properties Indexers Methods Structs Classes
For each member type above, order by access modifier as follows:
public internal protected internal protected private
Within the access modifiers, place static elements before instance (non-static) elements.
Place readonly elements before mutable elements.
Naming Conventions
Naming conventions enhance readability and describe domain objects throughout the system. At a glance, a developer should be able to determine the purpose of types, methods, etc. without having to look for context in the implementation.
DO use PascalCasing for class names, method names, properties and namespaces
DO use camelCasing for method arguments, local variables and field names
Exceptions to casing should include special business terminology used in domain objects. ReSharper might complain that the abbreviations have violated casing conventions. Add abbreviations to the ignore list in the ReSharper configuration file to skip analysis of domain abbreviations. Share DotSettings files at the team level.
DO NOT Use Hungarian Notation, prefixing data type abbreviations to variable or field names
DO use capitalization to separate two different words in the name of an identifier
AVOID Using abbreviations or shorter versions of words - as in GetWin for GetWindow
Exception: Abbreviations commonly used as names, such as Id, Xml, Ftp, Uri, etc.
DO use PascalCasing for abbreviations of three characters or more
DO NOT use underscores to separate words
Exception: Test method names are often written with underscores between words. In this specific instance, you are using underscores to increase readability of verbose and descriptive method names that would be hard to visually parse otherwise.
CONSIDER creating a list of exclusions in a shared ReSharper dotsettings file to hold well-known domain abbreviations
Example:
MASMModule where MASM stands for Medical Administrative Service Module
MROWeb where MRO stands for Medical Review Officer
DO use noun or noun phrases to name a class
AVOID using overly generic type names
A good name should describe everything a class or routine does. Some common suffixes to avoid are:
Manager Builder Writer Getter Setter Provider Facade
DO prefix interfaces with the letter ‘I’
Interface names should be nouns, noun phrases or adjectives.
Visibility of Types and Members
Favor more limiting over less limiting access modifiers. Use private for methods that are not to be accessed outside the current class, and public for methods that represent publishing an accessible interface and are not restricted. Limit access modifiers to prevent implementation details from leaking outside of the current context.
AVOID using internal or protected modifiers
Internal
modifiers are not typically useful unless you have a special use case, such as library development. Protected
modifiers may be an indicator that class inheritance is being abused.
DO use explicit modifiers, even when it matches the default access level
Example: Field declarations are private
by default, but adding an explicit private modifier clearly indicates that intent.
DO use private modifier on types or members that should only be accessed inside the class, and public for all others
When considering how to access types and their methods and properties, consider how testable the class is. If you over-limit your class with private modifiers, you make it difficult or impossible to write tests for all of its functionality.
DRY Principle
Organizing your code in smaller, maintainable, and reusable chunks makes the code stronger and less prone to errors, because each chunk of code is testable and performs its function as designed. Repeating code in multiple places is more prone to error and wastes time because you’ll have to modify the implementation in multiple places.
However, the DRY principle can also be overused. Overly “dry” code can be difficult to read, with too many logical calls and redirects to follow in order to understand what it actually does.
Additionally, when we write applications using feature folders & MediatR, we might find ourselves writing the same query or update operation in a few different handlers. It’s tempting to refactor that repetition out into some sort of utility or service, but first you need to determine if the repetition is a result of coincidence or intent. Command and Query handlers, for example, each represent a specific piece of business logic. Duplicate code across multiple handlers is more likely coincidental rather than intentional, and not a good candidate for refactoring into utility methods.
An approach you can consider is WET - “Write everything twice.” Code duplicated twice is usually OK, as it saves us from the effort of premature optimization based on gut reactions. Instead we look at patterns that emerge when code is duplicated three or more times. This allows us to examine the actual use case for the code and make intelligent decisions about how to properly refactor.
Additional Information: https://dev.to/wuz/stop-trying-to-be-so-dry-instead-write-everything-twice-wet-5g33
CONSIDER refactoring code into a common reusable method when that code has been duplicated three or more times
AVOID overusing the DRY principle to purposefully duplicated code
Code should be purposeful and exist to resolve a very specific problem. If refactoring seemingly duplicated code changes the characteristics of code, or the functionality, then leave it alone.
Use of Regions
Regions are an IDE feature from the .NET Framework 1.x era (c. 2002) that was used to hide generated designer code before 2.0 introduced partial classes. Code readability can be greatly hindered when your IDE hides details. If your source code file becomes too long then consider refactoring your implementation rather than hiding code in regions.
DO NOT use regions
Self-Documenting Code
Could should express intent through mindful naming and recognizable patterns. Self-describing code alleviates the need for forced or verbose commenting. Code comments should only be used to document implementation choices that are not self-explanatory such as terse language features like RegEx patterns or bit masks. If you think you need to write a comment, are you describing “why it is” or “what it is”? If you find the need to write a “what it is” comment, then you should reconsider the naming and structure of your code to better convey intent.
Additional sources:
Clean Code (Martin) chapters 2 (Meaningful Names) and 4 (Comments)
Headspring “Mindful Development - Week 4” video (~20:00 - 27:00 segment)(internal)
DO use mindful naming and recognizable patterns when naming methods, variables, etc. to express intent
Parameter names, for example, should be descriptive enough that the name and its type can be used to determine its use in most scenarios.
CONSIDER providing any additional information associated with your code in the commit text, rather than a comment block
Comment blocks grow stale, whereas the commit message lives with the history of the change.
DO NOT push TODO comments to main or dev branches
TODO
comments can be useful for a developer to track in-flight work, but they should never persist outside of the feature or bug branch. throw new NotImplementedException();
near the source of the TODO
to ensure that the work is completed before merging the branch.
For longer-lived “todo”s, track future work in JIRA, or the relevent project management tool, so that it can remain visible across the team, be planned, and tracked.
AVOID the use of comments to explain poorly written code
If you find yourself needing to write a comment to explain what you have done, that may be an indicator that some refactoring could be done to make the code more readable.
DO use comments when documentation is necessary or required by the client
XML reference documentation, for example, can be utilized by third-party tools such as Swagger to document a RESTful API.
DO NOT comment out code
Source control provides a history of code changes if a change needs to be reverted or referred back to.
Boy Scout Rule
In any project over time the quality of the code base will tend to degrade and technical debt will accrue. In an effort to proactively combat the accrual of technical debt, we make a conscientious effort to leave things better than we found it. If you’re making changes to a method, for example, take a few minutes and see if there is anything that can be improved in a short amount of time. The change doesn’t have to be big - just better. If you continue to make these little changes, then over time the overall quality of the code will dramatically improve.
Read Source Control Guidance - Make Tool Changes On Own Commit for details on how to capture refactor work in source control.
Additional sources:
Clean Code (Martin) chapters 1 (The Boy Scout Rule)
DO keep your changes in the scope of the feature or bug you are working on
Tech Leads need to be able to review your changes, and refactoring an unrelated class or method will make it more difficult and time consuming to determine the cause and impact of the change.
CONSIDER renaming variables, parameters, and methods
For example, variable names that are named ambiguously or incorrectly and require investigation to determine their purpose, such as day vs. birthDay, or price vs. priceAfterTax.
CONSIDER fixing small bugs you discover
Bugs discovered in scope of your feature that can be fixed quickly should be fixed. However, if you discover a larger bug, or bugs that are not in your feature’s scope, create a bug ticket instead.
CONSIDER refactoring large classes to increase readability
Large classes are often an indication of an over-generalized utility class.
CONSIDER potential merge or team conflicts for your changes
Refactoring code that other teams might be working on could cause cumbersome merge conflicts. Ensure your changes do not upset or delay overall progress.
CONSIDER creating “Refactor” or “Cleanup” tickets to track larger cleanup efforts you identify
These changes should be coordinated with the team and project leadership to ensure that they are not disruptive.
Composition vs Inheritance
Inheritance is an OOP pattern that can easily be abused.
Misuse of inheritance can lead to:
- Tight coupling between two concrete classes
- Fragile base classes
- Weakened encapsulation
- Issues or Complications with Testing
- Additional Maintenance Overhead
DO favor composition over inheritance
Composition gives our design higher flexibility, with the ability to modify behavior in the future without violating contracts.
DO use interfaces to define contracts between classes
If you define interfaces at key points of your application, you give careful thought to the behavior they should support and commit to that behavior.
DO NOT use inheritance when the subclass is not a proper sub-type of the superclass
Classes that use a lot of virtual
, override
and base
keywords are hard to read. Determining the purpose of a class that pulls or changes behavior from a base class (or a base of a base class) makes it difficult to track the actual behavior being performed. Subclasses should only be used to add to the functionality of a base class, not modify it.
DO NOT inherit across domain boundaries
If common mechanical structure is desired across domains, use a common utility base class instead.
CONSIDER single inheritance for establishing conventions in a single layer of an application
For example, we often create a base controller class that contains conventions we want available to all other controllers, such as a helper method for returning specifically formatted JSON responses.
AVOID inheritance of more than three levels
Inheritance of more than three levels is a strong indicator that you should consider refactoring to composition.
An example of acceptable three level inheritance is when you need to utilize a third party class before extending it with your own base implementation.
public class MyBaseController : ClientLibrary.ClientBaseController
{
// …
}
public class EmployeeController : MyBaseController
{
// …
}