As class libraries are typically intended to be used by a broad selection of developers who may not have insight into (or access to) the internal workings of the library, they introduce their own unique challenges. That said, while these are documented separately from our general C# Style Guide, Ignia considers these guidelines a best practice for all C# development.
Note: This style guide inherits rules from the C# Style Guide, C-Based Languages Style Guide, and the Global Style Guide.
Note: The majority of the items below are from the (excellent) book "Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (2nd Edition) (Krzysztof Cwalina, Brad Abrams; 2008) which is required reading for all developers at Ignia. The items highlighted below are some of the most important and common conventions from the book.
- Namespaces should use
<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]
convention; e.g.,Ignia.Localization.Import
(source) - Assemblies (DLLs) should generally be named
<Company>.(<Product>|<Technology>)[.<Feature>].dll
for consistency with namespaces; e.g.,Ignia.Localization.Import.dll
Note: Ignia uses
Ignia
for the<company>
on internal projects and reusable libraries, but the client name for custom client code
- Namespace identifiers should typically be pluralized (exceptions: proper names, abbreviations) (source)
- Do not use the same identifier for a namespace as well as a class (source)
- "If your brand employs nontraditional casing, you should follow the casing defined by your brand, even if it deviates from normal namespace casing" (source)
- Avoid generic identifier names that are likely to introduce conflicts with class names from related namespaces (source)
- Interfaces should begin with
I
and use adjective phrases (e.g.,ICacheable
); nouns are more appropriate for abstract classes (e.g.,Cache
) (source) Attribute
,EventHandler
,EventArgs
,Exception
,Collection
, andPermission
are all expected suffixes for corresponding class types (source)
- Only use public fields for constants; otherwise, expose properties (source)
- Consider using extension methods as a means of providing concrete implementations for general interface methods (source)
- Use the least derived type for method parameters; e.g., use an interface or base class when its properties are sufficient (source)
- Avoid using
out
orref
types for parameters (source) - Consider using the
params
keyword for (short) array parameters; otherwise, place arrays as the last parameter in a method so this can be applied in the future, if needed (source) - For serialization, prefer Data Contract Serialization unless control over the XML schema is needed (then use XmlSerializer) or the objects will be used via .NET Remoting (then use Runtime Serialization) (source)
- Avoid use of nested types
Dictionary<>
,Hashtable
,ArrayList
,List<>
, andIEnumerator
objects should not be exposed publicly (source)- Exception:
IEnumerator
may be returned as the result of aGetEnumerator()
method - Arrays should be avoided for similar reasons (source)
- Use the least derived collection type for parameters; ideally, this will be the
IEnumerable<T>
interface (source) - Do not expose setters for collection properties; this allows the collection to be overwritten, not just records (source)
- For writable properties and method return values, prefer
Collection<>
,ObservableCollection<>
(or a derived class); otherwise, use classes that implementICollection<>
,IList<>
, orIEnumerable<>
(source)- When practical, use a custom derived class for clarity of purpose, and to allow additional functionality to be added in the future
- When creating custom derived classes, prefer the naming convention
TypeCollection
whereType
maps to the item type (e.g.,AuthorCollection
is a collection ofAuthor
objects) - When exposing collections as properties, do not include the
Collection
suffix; instead, pluralize the type name (e.g.,Book.Authors
)
- For volatile collections (e.g., a file system object), return a snapshot (via a method) or an
IEnumerable<>
(via a property) to prevent exceptions during iteration - For read-only collections, always use
ReadOnlyCollection<>
(or a derived class) - Always return an empty collection rather than a
null
value for collection types; this makes it easier to test
- Prefer events to callbacks (e.g.,
Func<>
,Action<>
, andExpression<>
) as they are easier to understand and implement across languages (source) - Be wary of
virtual
members, as they provide potential security risks; when used, consider moving extensible functionality to aprotected
method to restrict scope (source)- When a key method delegates implementation details to a
protected
method for extensibility, be sure to note this in the documentation for the key method so the relationship is clear
- When a key method delegates implementation details to a
- Provide concrete classes for both
abstract
classes as well asinterface
definitions; this helps validate the design, and provides simpler options for casual implementations (source) - Consider providing reference tests for
abstract
classes andinterface
definitions to allow first- and third-party developers to easily test their implementations (source)
- Avoid throwing exceptions from properties; any state checking should be done when an action is performed (e.g., by calling a method)
- Validate arguments by throwing
ArgumentException
,ArgumentNullException
,InvalidOperationException
, or a derived class on error (source)- Always set the
paramName
property when throwingArgumentException
orArgumentNullException
; usevalue
when validating property setters (source)
- Always set the
- With the exception of unforeseeable problems (e.g., system failures), users should be able to pre-check conditions without throwing an exception via the Tester-Doer pattern (source)
- e.g., the
FileStream
class offers aCanWrite
property to conditionally determine if a stream can be written to - Fall back to the Try-Parse pattern for scenarios where providing pre-condition checks is not performant (e.g., a time-consuming conversion) (source)
- e.g., the
- Never throw a
NullReferenceException
,AccessViolationException
, orIndexOutOfRangeException
as resolving them relies on knowledge of the implementation; these should be caught as part of argument checking (source). - Do not throw or catch
Exception
orSystemException
; use more specific exceptions instead (exception: top-level exception handlers) (source)
- Prefer enums over arbitrary strings (e.g.,
Status="Complete"
) or static constants to set or receive values from a list of predefined (non-dynamic) choices - Prefer enums as method parameters to multiple Boolean arguments, or Boolean arguments which may have more options in the future (source)
- Enum names should be singular, unless they implement the
[flag]
attribute; enums should not be suffixed withEnum
orFlags
(source) - Only set enum values for capabilities currently supported; do not reserve values for future use (source)
- Always provide an enum value for
None
(or the semantic equivalent) (source) - Flag enums can be used to allow multiple values; assigned integers should be based on powers of two (2, 4, 8...) to allow proper integer serialization (source)
- Consider establishing a distinct unit test class for each class (e.g.,
MyClassTest
for tests related toMyClass
)- It is appropriate to combine related classes (e.g., collections, derived classes, &c.)
- Where appropriate, name unit tests
{Class}_{Member}{Test}
; e.g.,MyClass_SetValueTest
to testMyClass.SetValue()
- Be sure to test both with correct and incorrect input; e.g., ensuring the members fail as expected