Kleenheat C# Style Guide

Kleenheat uses the C# language when writing .NET applications.

The purpose of a style guide is not to wade into bike-shedding holy wars about tabs, spaces, braces, and other details. The purpose of a style guide is to achieve consistency, which improves the readability and maintainability of our C# code.

Kleenheat currently uses .NET Framework version 4.6 running on Windows servers.

1. Introduction

The C# Style Guide provides recommendations for writing C# code targeting the .NET framework. This guide has the following goals:

  • Minimize bugs, by reducing the chance of errors in logic or syntax.
  • Maximize maintainability, by having consistent, easy-to-understand code.

These guidelines are organized as simple recommendations prefixed with the terms DO, CONSIDER, AVOID, and DO NOT.

This guide should be followed for all C# code written for Kleenheat software. This means:

  • New code should adhere to these guidelines;
  • Legacy code, when worked on, should be updated to adhere to these guidelines; and
  • Third-party code snippets which are integrated into Kleenheat code should be updated to adhere to these guidelines.

This guide does not need to be followed for:

  • Third-party libraries which are not integrated into Kleenheat code (i.e., they remain as separate libraries).

Note that code examples in this document are written for compactness rather than for demonstrating good practice.

Where a guideline is not provided in this document, Microsoft’s C# Framework Design Guidelines should be considered.

2. Layout

2.1 Files

DO use a separate file for each class, except where it is a nested class or a private class.

2.2 Namespace Imports

DO start each file by importing required namespaces.

using System;
using Kleenheat.Common;

namespace Kleenheat.MicroService.Payment
{
}
namespace Kleenheat.MicroService.Payment
{
    using System;
    using Kleenheat.Common;
}

2.3 Regions

CONSIDER using a region to group members according to StyleCop ordering.

public class Customer
{
    #region Properties, Public
    //
    #endregion

    #region Methods, Public
    //
    #endregion

    #region Methods, Private
    //
    #endregion
}

2.4 Braces

DO place each brace on its own line (Eric Allman style).

foreach (var item in items)
{
    String message = $"Item '{item}'";
    Console.WriteLine(message);
}

DO wrap a single-line block of code in braces.

foreach (var item in items)
{
    Console.WriteLine(item);
}
foreach (var item in items) {
    Console.WriteLine(item);
}
foreach (var item in items)
    Console.WriteLine(item);
foreach (var item in items) { Console.WriteLine(item); }
foreach (var item in items) Console.WriteLine(item);

2.5 Indenting

DO use four spaces as indentation.

CONSIDER using more than four spaces to achieve “smart” indentation.

var permitted = (account.Balance <= 0) &&
                (account.DirectDebitEnabled);

2.6 Lines

DO write only one statement per line.

DO use an 80-character line length wherever possible.

2.7 Attributes

DO place each attribute on its own line.

[Description("MRS.")]
Mrs
[Description("MRS.")] Mrs

2.8 Spacing

DO use a single blank line between class methods and properties.

3. Comments

3.1 XML Documentation

DO document each class and each member, even if private, with standard XML comments. (If private, only the summary needs to be provided.)

DO surround each comment with a line of asterisks.

DO indent the content and closing tag of each section of the XML comments.

// *********************************************************************************************
//  GetFullName Method
//
/// <summary>
///     Gets the full name of the customer.
///     </summary>
/// <remarks>
///     The full name includes the customer’s first name and surname.
///     </remarks>
/// <param name="customer">
///     The customer whose full name is to be returned.
///     </param>
//
// *********************************************************************************************
public GetFullName(Customer customer)
{
}

3.2 Notes

CONSIDER adding notes below the XML documentation comments.

// *********************************************************************************************
//  GetFullName Method
//
/// <summary>
///     Gets the full name of the current Customer.
///     </summary>
/// <remarks>
///     The full name includes the customer’s first name and surname.
///     </remarks>
//  <notes>
//      1. If the customer’s name can be determined from  the Customer object, 
//         this value will be returned. Otherwise, the associated WebAccount
//         will be used to determine the customer’s name.
//         </notes>
//
// *********************************************************************************************
public GetFullName
{
}

3.3 Lines

DO place a comment on its own line, before the line of code to which it relates.

// Prices are shown using Australian dollars, regardless of current UI culture.
String text = price.ToString("F2", new CultureInfo("en-AU"));

CONSIDER placing a comment after a parameter when constructing a complex object.

return new FormsAuthenticationTicket(
    1,                  // The version number of the ticket.
    username,           // The user name associated with the ticket.
    now,                // The issue date (local date).
    now.AddMinutes(30), // The expiration date (local date).
    true,               // Indicates to persist the cookie across browser sessions.
    userData,           // The user data, serialized using JSON format.
    "/"                 // The path, meaning it applies throughout the domain.
);

3.4 Language

DO use American English for writing comments.

Color
Center
Normalize
Colour
Centre
Normalise

4. General Style

4.1 Arrays

DO prefer collections over arrays.

DO use byte arrays rather than a collection of bytes.

4.2 Disposable Objects

CONSIDER using the using statement for IDisposable objects.

using (varabel, even if it’s empty writer = new StringWriter())
{
    writer.Write("whatever");
}

4.3 If Statements

AVOID comparison to true or false.

if (customer.IsValid())
{
}
while (!finished)
{
}
if (customer.IsValid() == true)
{
}
while (finished == false)
{
}

CONSIDER removing a redundant else statement.

public String GetName(Customer customer)
{
    if (customer != null)
    {
        return customer.Name;
    }
    return "";
}
   
public String GetName(Customer customer)
{
    if (customer != null)
    {
        return customer.Name;
    }
    else
    {
        return "";
    }
}

4.4 Conditional Operators

AVOID nesting conditional operators.

String name = (customer != null) 
    ? (customer.Account != null) 
        ? customer.Account.Name 
        : customer.FullName
    : "";

4.5 Switch Statements

DO wrap each case in braces.

CONSIDER including a default label, even if it’s empty.

String name = "";
switch (product)
{
    case SFC:
    {
        name = "Standard Form Contract";
        break;
    }
    case MSS:
    {
        name = "Monthly Smart Saver";
        break;
    }
    default:
    {
        // unknown product
        break;
    }
}

4.6 Boolean Assignments

CONSIDER using parentheses to make an evaluation apparent.

Boolean active = (account.Status == Status.Active);
Boolean active = account.Status == Status.Active;

4.7 Return Statements

AVOID nesting by returning a value at the earliest opportunity.

public Promotion GetPromotion(String code)
{
    if (String.IsNullOrWhitespace(code))
    {
        return null;
    }
    Promotion promotion = // get from repository
    if (promotion == null)
    {
        return null;
    }
    if (!promotion.IsCurrent())
    {
        return null;
    }
    return promotion;
}
public Promotion GetPromotion(String code)
{
    if (!String.IsNullOrWhitespace(code))
    {
        Promotion promotion = // get from repository
        if (promotion != null)
        {
            if (promotion.IsCurrent())
            {
                return promotion;
            }
        }
    }
    return null;
}

4.8 Empty Strings

DO use "" rather than String.Empty.

String value = "";

return "";
String value = String.Empty;

return String.Empty;

4.9 Magic Numbers

DO NOT use magic numbers; use a constant.

const Int32 MONTHS_IN_YEAR = 12;
Decimal installment = (amount / MONTHS_IN_YEAR);
Decimal installment = (amount / 12);

5. Loops

5.1 Foreach Loops

CONSIDER using a foreach loop rather than a for loop, where possible.

foreach (var item in items)
{
}
for (Int32 i = 0; i < items.Count; i++)
{
    var item = items[i];
}

6. Types

6.1 Type Aliases

AVOID the use of type aliases, so that intrinsic types are presented the same way as higher-level types.

String name = "Robbie";
Int32 age = 25;
Person person = new Person(name, age);
string name = "Robbie";
int age = 25;
Person person = new Person(name, age);

6.2 Implicit Typing

CONSIDER the use of implicit types (var type) when the type is either obvious or not important.

foreach (var customer in customers)
{
    var address = customer.Address;
}
foreach (Customer customer in customers)
{
    Address address = customer.Address;
}

7. Naming

7.1 Language

DO use American English for naming variables.

color
center
normalize
colour
centre
normalise

7.2 Namespaces

DO use namespaces with Kleenheat as the first part, the category as the second part, and the library name as the third part.

namespace Kleenheat.Models.Payment
namespace Kleenheat.MicroSites.Payment
namespace Kleenheat.MicroServices.Payment

AVOID deep namespaces.

namespace Kleenheat.Website.DataAccess.Providers.Felix

7.3 Casing

DO use Pascal casing for namespace, class, and member names.

namespace Kleenheat.Stringray
{
    public class CustomerSummary
    {
        private void Initialize()
        {
        }
    }
}

DO use camel casing for parameters and variables.

public void Initialize(Customer customerAccount)
{
    Int32 id = customerAccount.ID;
    String accountName = customerAccount.Name;
}

DO use camel casing for an abbreviation or acronym of three or more letters.

UpdateUI() // two letters, okay to use uppercase
GetHex()
GetHtml()

DO use an underscore to prefix a private field in a class.

private Int32 _id;
private String _accountName;

CONSIDER using uppercase for constants.

public const String KEY = "Context.Key";

private const String MAX_RESULTS = 25;

DO use similar names for the default implementation of an interface.

public interface IFileReader { ... }

public class FileReader { ... }
  
public interface IFileReader { ... }

public class FileOpener { ... }

DO use underscores and “sentence casing” for unit tests.

[Test]
public void Ensure_a_null_value_returns_an_empty_string()
{
}
[Test]
public void EnsureANullValueReturnsAnEmptyString()
{
}

7.4 Word Choice

DO choose readability over brevity.

CanScrollHorizontally
ScrollableX

DO NOT use abbreviations.

UpdateWindow

numberCustomers
UpdateWin

numCustomers

DO NOT use acronyms, unless they are widely accepted.

SetNoticeOfCompletion

GetDateOfBirth
SetNoc

GetDob

7.5 Notations

DO NOT use Hungarian notation for variables.

String name
Int32 age
Decimal price
String strName
Int32 intAge
Decimal dPrice

DO NOT append ‘Class’ or ‘Struct’ to a class or structure.

7.6 Interfaces

DO prefix an interface name with the letter ‘I’.

public interface IReplayService 
{
}

7.7 Generics

DO use ‘T’ as the type parameter if possible.

public Boolean ImplementsInterface<T>(Type type) 
{
}

DO prefix a generic type with the letter ‘T’ for more than one type parameter.

public Boolean Implements<TImplementation, TContract>() 
{
}

7.8 Attributes

DO add the suffix ‘Attribute’ to an attribute class.

public class RequiredAttribute : Attribute
{
}

7.9 Exceptions

DO add the suffix ‘Exception’ to an exception class.

public class AccountException : ApplicationException
{
}

7.10 Enumerations

DO use the singular form for a non-flag enumeration.

public enum State
{
    Open,
    Closed
}

DO use the plural form for a flag enumeration.

[Flags]
public enum Privileges
{
    Create,
    Edit,
    Delete
}

8. Classes

8.1 Abstract Classes

DO NOT provide a public constructor in an abstract class.

DO provide a protected or internal constructor in an abstract class.

8.2 Order of Members

CONSIDER ordering members by type:

  1. Fields
  2. Constructors
  3. Properties
  4. Indexers
  5. Methods
  6. Classes

Within the above categories, order members by access modifier:

  • public
  • protected
  • internal
  • private

9. Structures

9.1 Structure Usage

DO NOT provide a default constructor for a struct.

DO ensure a struct is valid when all instance data is set to zero, false, or null.

10. Enumerations

10.1 Enumeration Usage

DO use an enum in favour of static constants.

DO NOT use an enum for open sets. An open set is one whose members will be added to over time (such as the list of a company’s products). A closed set is one whose members will not grow (such as days of the week, or months of the year).

10.2 Flags

DO use the [Flags] attribute on an enum if bitwise operations are to be performed.

DO use powers of two for flag enum values.

11. Interfaces

11.1 Interface Segregation

DO use many specific interfaces rather than a single general interface.

public interface IEntity 
{
    void SetMemento(Memento memento);
}

public interface IPersistable
{
    Memento GetMemento();
}

public interface IValidatable
{
    Boolean IsValid();
}
public interface IEntity 
{
    void SetMemento(Memento memento);
    Memento GetMemento();
    Boolean IsValid();
}

11.2 Interface Usage

DO use an interface rather than an implementation, where possible.

public void SortAlphabetically(IList<String> names) 
{
}

public IEnumerable<String> GetErrors()
{
}
public void SortAlphabetically(List<String> names)
{
}

Public List<String> GetErrors()
{
}

12 Constructors

12.1 Static Constructors

DO make static constructors private.

DO NOT throw exceptions from static constructors.

12.2 Instance Constructors

DO minimal work in the constructor.

DO use constructor parameters to initialize main properties.

AVOID calling virtual members from inside a constructor.

13 Methods

13.1 Parameters

DO validate parameters, throwing exceptions when validation fails.

CONSIDER using enums if a method would otherwise have two or more Boolean parameters.

CONSIDER using an object if a method would otherwise have multiple parameters.

public String ToHtml(HtmlFormattingOptions options)
{
}
public String ToHtml(String indent, Boolean minimized, Boolean quotedParameters, Boolean emptyAttributes)
{
}

DO NOT use ref parameters.

public void Clean(ref String value)
{
}

AVOID using out parameters.

public void GetParts(String input, out String part1, out String part2)
{
}

13.2 Extension Methods

AVOID extension methods on types, except when required by the framework (such as ASP.NET’s HtmlHelper class).

CONSIDER extension methods on interfaces.

13.3 Guard Clauses

DO use guard clauses to validate parameters.

public Decimal GetShippingPrice(Product product, Locaton location)
{
    if (product == null)
    {
        throw new ArgumentNullException(nameof(product));
    }
    if (location == null)
    {
        throw new ArgumentNullException(nameof(location));
    }
    if (product.IsObsolete())
    {
        throw new ProductObsoleteException(product.Name);
    }
    Decimal price = // logic to determine price
    return price;
}

13.3 Optional Parameters

DO use optional parameters rather than method overloads.

public String GetDisplayNoun(Int32 count, String noun, String suffix = "s")
{
    if (count > 1)
    {
        return noun + suffix;
    }
    return noun;
}
public String GetDisplayNoun(Int32 count, String noun)
{
    return GetDisplayNoun(count, noun, "s");
}

public String GetDisplayNoun(Int32 count, String noun, String suffix)
{
    if (count > 1)
    {
        return noun + suffix;
    }
    return noun;
}

14 Properties

14.1 Auto Properties

DO use auto properties when no additional logic is needed.

public String Name 
{
    get;
    set;
}
public String Name 
{
    get { return _name; }
    set { _name = value; }
}
private String _name;

DO place get and set accessors on separate lines when creating auto properties.

public String Name 
{
    get;
    set;
}
public String Name { get; set; }

CONSIDER placing the backing field below the property.

public String Name 
{
    get { return _name; }
    set { _name = Normalize(value); }
}
private String _name;

DO allow properties to be set in any order.

public String Name
{
    get { return _name; }
    set { _name = value; }
}
private String _name;

public String Phone
{
    get { return _phone; }
    set 
    {
        if (String.IsNullOrEmpty(_name))
        {
            throw new Exception("Name must be set before Phone");
        }
        _phone = value; 
    }
}
private String _phone;

CONSIDER lazy-loading of properties which are expensive to set.

public class Customer 
{
    public Account Account
    {
        if (_account == null)
        {
            _account = // fetch from database
        }
        return _account;
    }
    private Account _account;
}
public class Customer 
{
    public Customer()
    {
        _account = // fetch from database
    }

    public Account Account
    {
        return _account;
    }
    private Account _account;
}

DO NOT create a write-only property; use a method instead.

public void SetContext(Context context) 
{
    _context = context;
}
private Context _context;
public Context Context
{
    set { _context = value }
}
private Context _context;

14.2 Validity

DO preserve the previous value if a property setter throws an exception.

public String Name 
{
    get { return _name; }
    set 
    { 
        if (value == null)
        {
            throw new Exception("Name cannot be set to null"); 
        }
        _name = value; 
    }
}
private String _name;
public String Name 
{
    get { return _name; }
    set 
    { 
        _name = value; 
        if (value == null)
        {
            throw new Exception("Name cannot be set to null"); 
        }
    }
}
private String _name;

DO NOT throw exceptions from a property getter.

public String Name 
{
    get 
    { 
        if (name == null)
        {
            throw new Exception("Name must not be null"); 
        }
        return _name;
    }
    set { _name = value; }
}
private String _name;

15 Exceptions

DO NOT throw System.Exception or System.SystemException.

AVOID throwing a System.ApplicationException; use a more specific, descriptive exception if possible.

DO NOT return error codes—throw an exception instead.

public void SaveCustomer(Customer customer) 
{
    if (!customer.IsValid())
    {
        throw new CustomerException("Cannot save invalid customer");
    }
    // save customer
}
public Int32 SaveCustomer(Customer customer) 
{
    if (!customer.IsValid())
    {
        return 23; // error code
    }
    return 0;
}

CONSIDER using the tester-doer pattern when the doer method may throw an exception.

public class MailManager 
{
    private MailGateway _gateway;

    public Boolean IsConnected()
    {
        return (_gateway != null);
    }

    public void Send(Mail mail)
    {
        if (_gateway == null)
        {
            throw new ApplicationException("A MailGateway is not available");
        }
        _gateway.Send(mail);
    }
}

DO NOT throw an exception from a static constructor.

public class StringHelper 
{
    static StringHelper
    {
        throw new ApplicationException("Don’t throw from a static constructor");
    }
}

DO NOT throw an exception from a ToString method override.

public class Customer 
{
    public override String ToString()
    {
        throw new Exception("Don’t throw an exception from the ToString method");
    }
}

16 Strings

CONSIDER using string interpolation rather than String.Format or string concatenation to create a value.

String message = $"Welcome {name} to {company}.";
String message = String.Format("Welcome {0} to {1}.", name, company);
String message = "Welcome " + name + " to " + company + ".";

DO use a StringBuilder rather than concatenation when generating a long string value.

StringBuilder builder = new StringBuilder();
String separator = "";
foreach(var word in words)
{
    if (!Profane(word))
    {
        builder.Append(separator);
        builder.Append(word);
        separator = " ";
    }
}
String sentence = builder.ToString();
String sentence = "";
String separator = "";
foreach(var word in words)
{
    if (!Profane(word))
    {
        sentence += separator + word;
        separator = " ";
    }
}

17 Doubles

DO use Double.Epsilon to compare two Double values.

Double value1 = // first value
Double value2 = // second value
if (Math.Abs(value2 – value1) < Double.Epsilon)
{
    // value1 is equivalent to value2
}
Double value1 = // first value
Double value2 = // second value
if (value1 == value2)
{
    // may not evaluate as expected
}