Cratis C# engineering conventions
These are the house conventions Cratis maintainers apply across every
repository. They are conventions, not framework contracts: nothing here is
enforced by an analyzer unless this skill says so. Follow them for consistency;
do not claim the framework requires them.
Route near misses
- The question is what a Cratis product API does: resolve it against the
owning product repository, not against this style guide.
- The subject is a specification file: the specification conventions own the
Establish/Because/should_ pattern and the for_/when_ hierarchy.
- The subject is TypeScript or React: this skill covers C# only.
- The subject is repository structure or documentation: those are separate
workflows.
Quick reference
- Use current C# language features — records, primary constructors, pattern
matching, collection expressions.
var over an explicit type; the right-hand side already names the type.
- File-scoped namespace declarations.
using directives alphabetically sorted, single-line, unused ones removed.
- No regions. A file that needs them needs refactoring instead.
- No technical postfixes on type names: no
Impl, Service, Manager,
Handler, Base, Async.
- No
Exception suffix on exception types — AuthorNotFound, not
AuthorNotFoundException.
- Never throw a built-in exception type. Always define a domain exception.
record for events, commands, read models, concepts, and DTOs.
is null and is not null — never == null or != null.
- Blank line before the opening
{ of every block.
- A final
return sits on its own line.
- Private fields are
_camelCase; interfaces take the I prefix.
- American English everywhere — initialize, behavior, color, serialize.
- Every file starts with the repository license header.
Formatting
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace <RootNamespace>.<Feature>;
using <Namespace>.<First>;
using <Namespace>.<Second>;
// Blank line before the opening brace of every block
if (<condition>)
{
<statement>;
}
// Expression-bodied form for simple members
public string <PropertyName> => $"{<First>} {<Second>}";
// The final return stands alone
public <ReturnType> <MethodName>()
{
var result = <expression>;
return result;
}
Naming
| Artifact |
Convention |
Example |
| Type, method, public member |
PascalCase |
RegisterAuthor, AuthorId |
| Private field |
_camelCase |
_eventLog |
| Local variable |
camelCase |
authorId |
| Interface |
I prefix |
IEventLog |
| Exception type |
No Exception suffix |
AuthorNotFound |
| Feature folder |
Pluralized domain noun |
Authors/ |
| Concept file |
The concept name |
AuthorId.cs |
Avoid abbreviations unless they are universally known (Id, Xml, Json,
Url). Never add a prefix or postfix that names a technical role —
Controller, ViewModel, Handler, Manager, Factory, Base. Name after
the domain, not the pattern.
Where the detail lives
| Topic |
Reference |
Records, primary constructors, var, collections, nullable, async, pattern matching, XML documentation |
code-style.md |
| Custom exceptions, structured logging, dependency injection, service lifetimes, implementation discovery |
exceptions-logging-and-di.md |
| CUPID, cohesion over layers, ubiquitous language, immutability |
domain-philosophy.md |
Read the reference that covers the decision at hand rather than all three.
The two rules most often got wrong
[Singleton] is a narrow choice, not the default. A singleton may not
depend on anything that belongs to a tenant, a user, or a request. Capturing a
scoped collaborator does not throw — it silently binds to the root scope's
default namespace forever and returns empty results. See
exceptions-logging-and-di.md.
Use IInstancesOf<T>, never IEnumerable<T>, to enumerate implementations of
an abstraction. IEnumerable<T> only works when every implementation is
hand-registered, which defeats convention-based discovery.
Verify
- Every file carries the repository license header and a file-scoped namespace.
using directives are sorted, single-line, and free of unused entries.
- No regions, no technical postfixes, no
Exception suffix.
- Every thrown exception is a domain type deriving from
Exception with a
meaningful message and an XML <exception> or <summary> doc starting with
"The exception that is thrown when".
- No
catch block is empty or silently swallowing.
- Null checks use
is null / is not null, and no defensive check contradicts a
non-nullable annotation.
- Every public type, method, property, and operator carries multiline XML
documentation with
<param> and <returns> where applicable.
- No
[Singleton] holds tenant-, user-, or request-bound state.
- No
services.Add*<TInterface, TImplementation>() registers a type that exists
to be discovered by convention.
- Text is American English.
- The solution builds with zero warnings and zero errors, and the affected
specifications pass.
1---2name: cratis-engineering-csharp-conventions3description: Apply the Cratis C# house conventions when writing or reviewing C# in a Cratis repository - formatting, naming, records and primary constructors, nullable handling, XML documentation, custom exceptions, structured logging, dependency injection, and service lifetimes. Use for any "how should this be written" C# style question; defer product API decisions and specification authoring to their focused workflows.4license: LICENSE5---67# Cratis C# engineering conventions89These are the house conventions Cratis maintainers apply across every10repository. They are **conventions**, not framework contracts: nothing here is11enforced by an analyzer unless this skill says so. Follow them for consistency;12do not claim the framework requires them.1314## Route near misses1516- The question is what a Cratis product API *does*: resolve it against the17 owning product repository, not against this style guide.18- The subject is a specification file: the specification conventions own the19 `Establish`/`Because`/`should_` pattern and the `for_`/`when_` hierarchy.20- The subject is TypeScript or React: this skill covers C# only.21- The subject is repository structure or documentation: those are separate22 workflows.2324## Quick reference2526- Use current C# language features — records, primary constructors, pattern27 matching, collection expressions.28- `var` over an explicit type; the right-hand side already names the type.29- File-scoped namespace declarations.30- `using` directives alphabetically sorted, single-line, unused ones removed.31- No regions. A file that needs them needs refactoring instead.32- No technical postfixes on type names: no `Impl`, `Service`, `Manager`,33 `Handler`, `Base`, `Async`.34- No `Exception` suffix on exception types — `AuthorNotFound`, not35 `AuthorNotFoundException`.36- Never throw a built-in exception type. Always define a domain exception.37- `record` for events, commands, read models, concepts, and DTOs.38- `is null` and `is not null` — never `== null` or `!= null`.39- Blank line before the opening `{` of every block.40- A final `return` sits on its own line.41- Private fields are `_camelCase`; interfaces take the `I` prefix.42- American English everywhere — initialize, behavior, color, serialize.43- Every file starts with the repository license header.4445## Formatting4647```csharp48// Copyright (c) Cratis. All rights reserved.49// Licensed under the MIT license. See LICENSE file in the project root for full license information.5051namespace <RootNamespace>.<Feature>;5253using <Namespace>.<First>;54using <Namespace>.<Second>;5556// Blank line before the opening brace of every block57if (<condition>)58{59 <statement>;60}6162// Expression-bodied form for simple members63public string <PropertyName> => $"{<First>} {<Second>}";6465// The final return stands alone66public <ReturnType> <MethodName>()67{68 var result = <expression>;6970 return result;71}72```7374## Naming7576| Artifact | Convention | Example |77| --- | --- | --- |78| Type, method, public member | PascalCase | `RegisterAuthor`, `AuthorId` |79| Private field | `_camelCase` | `_eventLog` |80| Local variable | camelCase | `authorId` |81| Interface | `I` prefix | `IEventLog` |82| Exception type | No `Exception` suffix | `AuthorNotFound` |83| Feature folder | Pluralized domain noun | `Authors/` |84| Concept file | The concept name | `AuthorId.cs` |8586Avoid abbreviations unless they are universally known (`Id`, `Xml`, `Json`,87`Url`). Never add a prefix or postfix that names a technical role —88`Controller`, `ViewModel`, `Handler`, `Manager`, `Factory`, `Base`. Name after89the domain, not the pattern.9091## Where the detail lives9293| Topic | Reference |94| --- | --- |95| Records, primary constructors, `var`, collections, nullable, async, pattern matching, XML documentation | [code-style.md](references/code-style.md) |96| Custom exceptions, structured logging, dependency injection, service lifetimes, implementation discovery | [exceptions-logging-and-di.md](references/exceptions-logging-and-di.md) |97| CUPID, cohesion over layers, ubiquitous language, immutability | [domain-philosophy.md](references/domain-philosophy.md) |9899Read the reference that covers the decision at hand rather than all three.100101## The two rules most often got wrong102103**`[Singleton]` is a narrow choice, not the default.** A singleton may not104depend on anything that belongs to a tenant, a user, or a request. Capturing a105scoped collaborator does not throw — it silently binds to the root scope's106default namespace forever and returns empty results. See107[exceptions-logging-and-di.md](references/exceptions-logging-and-di.md).108109**Use `IInstancesOf<T>`, never `IEnumerable<T>`, to enumerate implementations of110an abstraction.** `IEnumerable<T>` only works when every implementation is111hand-registered, which defeats convention-based discovery.112113## Verify114115- Every file carries the repository license header and a file-scoped namespace.116- `using` directives are sorted, single-line, and free of unused entries.117- No regions, no technical postfixes, no `Exception` suffix.118- Every thrown exception is a domain type deriving from `Exception` with a119 meaningful message and an XML `<exception>` or `<summary>` doc starting with120 "The exception that is thrown when".121- No `catch` block is empty or silently swallowing.122- Null checks use `is null` / `is not null`, and no defensive check contradicts a123 non-nullable annotation.124- Every public type, method, property, and operator carries multiline XML125 documentation with `<param>` and `<returns>` where applicable.126- No `[Singleton]` holds tenant-, user-, or request-bound state.127- No `services.Add*<TInterface, TImplementation>()` registers a type that exists128 to be discovered by convention.129- Text is American English.130- The solution builds with zero warnings and zero errors, and the affected131 specifications pass.