Miscellaneous Design Guidelines

Edit this page

Use exceptions for errors when the error case is exceptional (RG1200)

A code base that uses return values to report success or failure tends to have nested if-statements sprinkled all over the code. Quite often, a caller forgets to check the return value anyway. Structured exception handling has been introduced to allow you to throw exceptions and catch or replace them at a higher layer. In most systems it is quite common to throw exceptions whenever an unexpected situation occurs.

Only use exceptions for exceptional circumstances. For example, if you have a bool returning function which returns false half the time, you don’t want to use exceptions for the false case even if it does technically describe an error condition.

Provide a rich and meaningful exception message text (RG1202)

The message should explain the cause of the exception, and clearly describe what needs to be done to avoid the exception.

Throw the most specific exception that is appropriate (RG1205)

For example, if a method receives a null argument, it should throw ArgumentNullException instead of its base type ArgumentException.

Don’t swallow errors by catching generic exceptions (RG1210)

Avoid swallowing errors by catching non-specific exceptions, such as Exception, SystemException, and so on, in application code. Only in top-level code, such as a last-chance exception handler, you should catch a non-specific exception for logging purposes and a graceful shutdown of the application.

Properly handle exceptions in asynchronous code (RG1215)

When throwing or handling exceptions in code that uses async/await or a Task remember the following two rules:

  • Exceptions that occur within an async/await block and inside a Task’s action are propagated to the awaiter.
  • Exceptions that occur in the code preceding the asynchronous block are propagated to the caller.

For more details see Best Practices in Asynchronous Programming.

Always check an event handler delegate for null (RG1220)

An event that has no subscribers is null. So before invoking, always make sure that the delegate list represented by the event variable is not null. Invoke using the null conditional operator, because it additionally prevents conflicting changes to the delegate list from concurrent threads.

event EventHandler<NotifyEventArgs> Notify;

protected virtual void OnNotify(NotifyEventArgs args)
{
	Notify?.Invoke(this, args);
}

Use a protected virtual method to raise each event (RG1225)

Complying with this guideline allows derived classes to handle a base class event by overriding the protected method. The name of the protected virtual method should be the same as the event name prefixed with On. For example, the protected virtual method for an event named TimeChanged is named OnTimeChanged.

Note: Derived classes that override the protected virtual method are not required to call the base class implementation. The base class must continue to work correctly even if its implementation is not called.

Consider providing property-changed events (RG1230)

Consider providing events that are raised when certain properties are changed. Such an event should be named PropertyChanged, where Property should be replaced with the name of the property with which this event is associated

Note: If your class has many properties that require corresponding events, consider implementing the INotifyPropertyChanged interface instead. It is often used in the Presentation Model and Model-View-ViewModel patterns.

Don’t pass null as the sender argument when raising an event (RG1235)

Often an event handler is used to handle similar events from multiple senders. The sender argument is then used to get to the source of the event. Always pass a reference to the source (typically this) when raising the event. Furthermore don’t pass null as the event data parameter when raising an event. If there is no event data, pass EventArgs.Empty instead of null.

Exception: On static events, the sender argument should be null.

Use generic constraints if applicable (RG1240)

Instead of casting to and from the object type in generic types or methods, use where constraints or the as operator to specify the exact characteristics of the generic parameter. For example:

class SomeClass  
{}

// Don't  
class MyClass  
{
	void SomeMethod(T t)  
	{  
		object temp = t;  
		SomeClass obj = (SomeClass) temp;  
	}  
}

// Do  
class MyClass where T : SomeClass  
{
	void SomeMethod(T t)  
	{  
		SomeClass obj = t;  
	}  
}

Know the difference between IEnumerable and IReadOnlyCollection (RG1250)

A caller that gets an IEnumerable<T> returned to it should expect that enumerating the returned object may entail deferred execution. The IEnumerable<T> should be enumerated at most once.

When writing a method that returns IEnumerable<T>, be aware that the method’s callers will have to enumerate the object themselves. In the event that the object is always enumerated before it is returned, this leads to unnecessary code in the caller. In this case, the method should return IReadOnlyCollection<T> instead. Return IEnumerable<T> only if you intend to take advantage of deferred execution.

Do not use this and base prefixes unless it is required (RG1251)

In a class hierarchy, it is not necessary to know at which level a member is declared to use it. Refactoring derived classes is harder if that level is fixed in the code.

A green build should be releasable (RG1260)

There should be enough rigour in our build and test pipeline to provide confidence that a green build is of sufficient quality to be released to our customers without further inspection.

Note the level of confidence required may depend on the lifecycle stage of the product, and this does not preclude investigative work as part of building software.