| name | csharp-style |
| description | Applies C# coding conventions when writing, reviewing, or refactoring code. Covers .cs files, XML documentation, naming, formatting, error handling, using directives, file ordering, and Unity-specific patterns. Use when writing new C# code, modifying existing code, reviewing pull requests, or when the user asks about C# coding standards. |
| user-invocable | false |
C# code style guide
Applies C# coding conventions.
You MUST read this skill and load the relevant reference files before writing or modifying C#
code. You MUST verify your changes against the checklist before submitting.
Scope
Covers:
- C# code style (XML documentation, naming, formatting, error handling)
- Using directive conventions and file organization
- Class design, enums, properties, and inheritance patterns
- Unity-specific patterns (MonoBehaviour, ScriptableObject, serialization)
- CSharpier and EditorConfig tooling conventions
- Cross-language consistency with C++ and Python conventions
Does not cover:
- README file conventions (see
/readme-style)
- Commit message conventions (see
/commit)
- Skill file and CLAUDE.md conventions (see
/skill-design)
- Codebase exploration workflows (see
/explore-codebase)
Workflow
You MUST follow these steps when this skill is invoked.
Step 1: Read this skill
Read this entire file. The core conventions below apply to ALL C# code.
Step 2: Load relevant reference files
Based on the task, load the appropriate reference files:
| Task | Reference to load |
|---|
| Writing or modifying XML docs / type usage / comments | xml-docs-and-types.md |
| Writing classes, enums, or Unity components | class-patterns.md |
| Using LINQ, async, IDisposable, testing; function calls, blank lines, formatting, tooling, config files, guard clauses | libraries-and-tools.md |
| Deploying or verifying tool config files | assets/ directory |
| Reviewing code before submission | anti-patterns.md |
Load multiple references when the task spans multiple domains.
Step 3: Apply conventions
Write or modify C# code following all conventions from this file and the loaded references.
Step 4: Verify compliance
Complete the verification checklist at the end of this file. Every item must pass before
submitting work. For anti-pattern examples, load
anti-patterns.md.
Cross-language consistency
Projects span Python, C++, and C#. These conventions maximize visual and structural
consistency across languages while respecting each language's idiomatic standards.
Shared across all languages:
- 120 character line limit
- 4-space indentation (no tabs)
- Comprehensive documentation on ALL public and private members
- Third-person imperative mood for documentation ("Provides...", "Determines whether...")
- Private members use underscore prefix (
_snake_case in Python and C++, _camelCase in C#)
- Full words in identifiers (no abbreviations)
- Guard clauses preferred over deep nesting
- Prose over bullet lists in documentation
- No example/code blocks in documentation (they go stale)
- I/O operations separated from processing logic
Shared between C++ and C# only:
- Allman brace style (opening braces on new lines; Python uses indentation)
C#-specific divergences from C++:
- Constants use PascalCase (not
kPrefix as in C++)
- Enum values use PascalCase (not
kPrefix as in C++)
- Namespaces use PascalCase (not snake_case as in C++)
- Consecutive assignment alignment is NOT used (CSharpier does not support it)
#region blocks are NOT used (prefer blank lines between logical groups)
C#-specific divergences from Python:
- Methods and properties use PascalCase (not snake_case as in Python)
- Constants use PascalCase (not
_UPPER_SNAKE_CASE as in Python)
- Enum values use PascalCase (not
UPPER_SNAKE_CASE as in Python)
- Private members use
_camelCase (not _snake_case as in Python)
- Public fields use camelCase (not snake_case as in Python)
- Documentation uses XML
<summary> tags (not Google-style docstrings)
Naming conventions
Variables
Use full words, not abbreviations:
| Avoid | Prefer |
|---|
pos, idx | position, index |
msg, val | message, value |
seg, trig | segment, trigger |
cfg, cnt | configuration, count |
Identifiers
| Element | Convention | Example |
|---|
| Classes | PascalCase | StimulusTriggerZone, TaskTemplate |
| Methods | PascalCase | ResetState, GetSegmentLengths |
| Public fields | camelCase | trackLength, requireLick, isActive |
| Public properties | PascalCase | IsOccupancyMode, CorridorSpacingUnity |
| Private fields | _camelCase | _currentSegmentIndex, _occupancyTimer |
| Local variables | camelCase | segmentPath, measuredLength |
| Parameters | camelCase | configPath, qosLevel |
| Constants | PascalCase | LengthComparisonEpsilon |
| Enum types | PascalCase | ControllerTypes, StimulusMode |
| Enum values | PascalCase | LinearTreadmill, OccupancyBased |
| Namespaces | PascalCase | Gimbl, Project.Config |
| Interfaces | IPascalCase | IConfigurable, IResettable |
| Type parameters | TPascalCase | TMessage, TConfig |
Public fields vs properties
In Unity projects, MonoBehaviour and ScriptableObject classes expose public fields using
camelCase for Inspector serialization. These are effectively configuration parameters set via the
Unity Editor:
public bool requireLick = false;
public float trackLength = 10f;
Use PascalCase properties for computed values or encapsulated access:
public bool IsOccupancyMode => _occupancyZone != null;
Functions
- Use descriptive verb phrases:
CreateNewTask, ValidateTemplate, ResetState
- Private methods follow PascalCase (no underscore prefix, unlike Python)
- Avoid generic names like
Process, Handle, DoSomething
Constants and immutability
Use PascalCase for const and static readonly fields. Prefer immutability by default:
const: Compile-time constants (primitives, strings). Inlined at call sites.
static readonly: Runtime-initialized immutable values (objects, computed values).
readonly: Instance fields assigned only in constructors or field initializers.
private const float LengthComparisonEpsilon = 0.01f;
private static readonly string DefaultConfigPath = Path.Combine(Application.dataPath, "config");
private readonly SerialPort _port;
You MUST mark fields as readonly when they are only assigned in the constructor or initializer.
For detailed immutability patterns (readonly struct, records, in parameters), see
class-patterns.md.
Function calls
See libraries-and-tools.md for named argument conventions.
Error handling
Unity projects
Use Unity's logging system for error reporting:
if (template == null)
{
Debug.LogError("Failed to load task template from YAML file.");
return;
}
if (Mathf.Abs(measuredLength - configuredLength) > LengthComparisonEpsilon)
{
Debug.LogWarning(
$"For {segmentName}, mismatch between prefab length ({measuredLength}) "
+ $"and configured length ({configuredLength})."
);
}
Non-MonoBehaviour code
In editor tools, static utility classes, and pure C# libraries that do not inherit from
MonoBehaviour, use standard C# exceptions:
public static TaskTemplate ParseTemplate(string yamlContent)
{
if (string.IsNullOrEmpty(yamlContent))
{
throw new ArgumentException("YAML content must not be null or empty.", nameof(yamlContent));
}
TaskTemplate template = YamlParser.Deserialize<TaskTemplate>(yamlContent);
if (template == null)
{
throw new FormatException("Failed to deserialize task template from YAML content.");
}
return template;
}
When to use each approach
| Context | Error mechanism | Example |
|---|
| MonoBehaviour lifecycle methods | Debug.LogError + return | Task.Start(), OccupancyZone.Update() |
| Editor tools and menu commands | Exceptions | CreateTask.NewTask() |
| Static utility methods | Exceptions | Utility.GetSegmentLengths() |
| Pure C# classes (non-Unity) | Exceptions | ConfigParser.Parse() |
| MonoBehaviour constructors/Awake | Debug.LogError + return | Field validation in Awake() |
| Plain C# class constructors | Exceptions | Communication(portName, baudRate) |
Error message format
Use a structured format: context ("Unable to..."), constraint ("must be..."), actual value
("but [actual state]."). Use Debug.LogError() for failures that prevent continuation,
Debug.LogWarning() for non-critical issues, and Debug.Log() for informational messages.
For multi-line error messages, assign the message to a local variable before passing it.
This matches the Python convention of assigning to a message variable before calling
console.error():
string message =
$"Unable to create corridor for segment '{segmentName}'. "
+ $"The configured length ({configuredLength}) must match the prefab length "
+ $"({measuredLength}), but they differ by {Mathf.Abs(measuredLength - configuredLength)}.";
Debug.LogError(message);
Debug.LogError("Failed to load task template from YAML file.");
Null handling
- Use explicit null checks:
if (template == null)
- Use
string.IsNullOrEmpty() for string validation
- Use
TryGetComponent<T>() for safe component access in Unity
- Prefer null-conditional operator (
?.) for optional chains
- Prefer null-coalescing operator (
??) for default values
Comments
See xml-docs-and-types.md for inline comment conventions and what to avoid.
Using directives
- All
using directives must be at the top of the file, outside the namespace
- System directives first, then third-party, then project-local
- Sorting is enforced by EditorConfig (
dotnet_sort_system_directives_first = true)
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Gimbl;
using Project.Config;
using static directives
Do NOT use using static directives. Always qualify static method calls with the type name
for clarity:
using static UnityEngine.Mathf;
float result = Clamp(value, 0f, 1f);
float result = Mathf.Clamp(value, 0f, 1f);
Global usings
Do NOT use C# 10 global using directives or implicit usings. Every file must contain its
own explicit using directives. This ensures each file is self-contained and matches the C++
convention of explicit #include directives per file.
File-level ordering
All definitions within a file follow this vertical ordering from top to bottom:
- File-level XML documentation (
/// <summary> block describing the file)
- Using directives
- Namespace declaration (block-scoped is the project convention:
namespace Project.Config { ... },
with class members indented one level inside the block). Although assets/.editorconfig carries
csharp_style_namespace_declarations = file_scoped:suggestion, the exemplar code overrides it and
block-scoped is the practiced form.
- Enumerations (type definitions that other code depends on)
- Class declaration with members in this order:
a. Constants (
const and static readonly fields)
b. Public fields (Unity Inspector-serialized)
c. Private fields (_camelCase)
d. Properties
e. Unity lifecycle methods (Awake, Start, Update, OnDestroy)
f. Public methods
g. Private methods
h. Nested classes
Visibility ordering
Within each member kind, order by visibility: public -> internal -> protected ->
private. Always write access modifiers explicitly — never rely on C#'s implicit private
default. Unity lifecycle methods appear in their natural execution order regardless of
visibility.
Call-hierarchy ordering
Within each visibility group, definitions should loosely follow the order in which they are
called during the class's runtime. When there is no clear call hierarchy, group definitions
by purpose. This matches the Python convention of ordering definitions by call sequence
within each visibility group.
For MonoBehaviour classes, this naturally follows from the lifecycle ordering: Awake calls
initialization helpers, Start calls setup helpers, Update calls per-frame helpers.
One class per file
Each .cs file should contain exactly one public type. The file name must match the class
name (e.g., OccupancyZone.cs contains class OccupancyZone). Nested helper classes and
message types may remain in the containing class's file.
Guard clauses and boolean expressions
See libraries-and-tools.md for guard clause and boolean expression conventions.
Blank lines
See libraries-and-tools.md for blank line conventions.
Formatting, tooling, and configuration files
See libraries-and-tools.md for line length, formatting, and brace
rules; CSharpier and EditorConfig tooling; and configuration file references.
Related skills
| Skill | Relationship |
|---|
/python-style | Provides Python conventions; C# conventions parallel these |
/cpp-style | Provides C++ conventions; C# conventions parallel these |
/project-layout | Provides C# Unity directory tree; invoke for project structure |
/readme-style | Provides README conventions; invoke for README tasks |
/commit | Provides commit message conventions; invoke for commit tasks |
/skill-design | Provides skill file conventions; invoke for skill authoring tasks |
/explore-codebase | Provides project context that informs style-compliant code changes |
Proactive behavior
When reviewing or modifying C# code, proactively check for style violations and fix them. When
writing new code, apply all conventions from this skill and its references without being asked.
If you notice existing code near your changes that violates conventions, mention it to the user
but do not fix it unless asked.
Verification checklist
You MUST verify your edits against this checklist before submitting any changes to C# files.
C# Style Compliance:
- [ ] XML documentation on all public and private members
- [ ] Summary tags use third-person imperative mood ("Provides..." not "This class provides...")
- [ ] Boolean members documented with "Determines whether..."
- [ ] File-level XML summary comment present
- [ ] Sentences in comments and XML docs stay under 40 words
- [ ] XML doc length proportional to method complexity (no multi-paragraph remarks on simple properties)
- [ ] <param> and <returns> descriptions do not restate type information
- [ ] XML doc accurately describes the method's observable behavior
- [ ] Comments and XML docs free of typos and grammar errors
- [ ] Inline comments explain why, not what (no narrate-the-code comments)
- [ ] No stale references in comments (closed issues, removed code, outdated TODOs)
- [ ] All lines ≤ 120 characters
- [ ] 4-space indentation, no tabs
- [ ] Allman brace style (opening braces on new lines)
- [ ] LF line endings
- [ ] Full words used (no abbreviations like pos, idx, val, msg)
- [ ] Private fields use _camelCase prefix
- [ ] Public fields use camelCase (Unity serialized fields)
- [ ] Public properties use PascalCase
- [ ] Constants use PascalCase (const and static readonly)
- [ ] Methods use PascalCase (both public and private)
- [ ] Enum types and values use PascalCase
- [ ] Access modifiers always explicit (never rely on implicit private)
- [ ] Using directives at top of file, outside namespace
- [ ] System directives sorted first
- [ ] String interpolation used (not string.Format or concatenation)
- [ ] Named arguments used for boolean params and ambiguous calls
- [ ] Null checks use explicit comparison (== null, != null)
- [ ] Guard clauses / early returns preferred over deep nesting
- [ ] Fields marked readonly when only assigned in constructor/initializer
- [ ] No #region blocks; no this. qualifier (except disambiguation)
- [ ] No IDE suppression comments (ReSharper/Rider // ReSharper disable etc.); use #pragma warning / [SuppressMessage]
- [ ] One public type per file; file name matches class name
- [ ] Unity logging uses Debug.LogError/LogWarning/Log appropriately
- [ ] XML tag ordering: summary → remarks → typeparam → param → returns → exception
- [ ] <exception> tags in alphabetical order by type name
- [ ] <inheritdoc/> used only when base documentation fully describes override behavior
- [ ] No <example> or <code> tags in XML documentation
- [ ] No using static directives; no global usings
- [ ] ToString format matches ClassName(key=value, key=value) pattern
- [ ] in used only for large readonly struct parameters (not small types or reference types)
- [ ] out parameters follow TryX pattern (return bool, populate out on success)
- [ ] Error mechanism matches context (Debug.LogError in MonoBehaviour; exceptions elsewhere)
- [ ] Multi-line error messages assigned to variable before passing
- [ ] Prose used in <remarks> blocks (not bullet lists)
- [ ] Property summaries are single sentences (no summary + remarks split)
- [ ] Test methods have only <summary> (no <param>, <returns>, or <exception> tags)
- [ ] I/O operations separated from processing logic
- [ ] Path.Combine used for path construction (not string concatenation)
- [ ] StringComparison.Ordinal used for internal string matching
- [ ] Static methods used when no instance state is accessed
- [ ] Methods ordered by call hierarchy within each visibility group
- [ ] CSharpier formatting applied before commit
- [ ] Inline comments use third person imperative
Unity-Specific Compliance:
- [ ] MonoBehaviour fields for Inspector use public camelCase
- [ ] Private backing fields use [SerializeField] when Inspector access needed
- [ ] Lifecycle methods in execution order (Awake, Start, Update, OnDestroy)
- [ ] TryGetComponent used for safe component access
- [ ] No unnecessary GetComponent calls in Update loops
- [ ] No LINQ in Update/FixedUpdate (allocations in hot paths)
- [ ] IDisposable resources cleaned up in OnDestroy
- [ ] Event subscriptions have matching unsubscriptions (OnEnable↔OnDisable, Start↔OnDestroy)
- [ ] Coroutines stopped in OnDisable/OnDestroy to prevent orphaned execution
- [ ] #if UNITY_EDITOR used for editor-only code; [Conditional] preferred over #if DEBUG
- [ ] Switch expressions used for pure value mapping; switch statements for side effects