C# Idioms and Patterns
C# rewards type safety, LINQ expressiveness, and async-first design. Modern C# (10+/.NET 6+) favors records, nullable reference types, and minimal APIs. Idiomatic C# = clean, async-aware, framework-integrated.
Scope: C# coding idioms. Test naming: GEMINI.md § Testing Strategy. Logging: @.gemini/skills/logging-and-observability-principles/SKILL.md.
Modern C# Features (10+)
Nullable reference types — always enabled:
// ✅ Explicit nullability
public Task? FindById(string id) { ... }
public Task GetById(string id) { ... } // never returns null — throws
// In .csproj: <Nullable>enable</Nullable>
Records for immutable data:
public record CreateTaskRequest(string Title, Priority Priority);
public record TaskResponse(string Id, string Title, DateTime CreatedAt);
Pattern matching:
return result switch
{
Success(var task) => Ok(task),
NotFound(var id) => NotFound($"Task {id} not found"),
ValidationError(var errors) => BadRequest(errors),
_ => StatusCode(500)
};
required and init for safe construction:
public class AppConfig
{
public required string DatabaseUrl { get; init; }
public required string ApiKey { get; init; }
public int MaxRetries { get; init; } = 3;
}
Error Handling
Result pattern over exceptions for expected failures:
public record Result<T>
{
public T? Value { get; init; }
public string? Error { get; init; }
public bool IsSuccess => Error is null;
public static Result<T> Ok(T value) => new() { Value = value };
public static Result<T> Fail(string error) => new() { Error = error };
}
Domain exceptions for unexpected failures — never raw Exception.
Never catch (Exception) without re-throw or specific handling.
Async/Await
Async all the way — never .Result or .Wait() on tasks:
// ✅ Async pipeline
public async Task<Task> GetTaskAsync(string id, CancellationToken ct)
{
return await _storage.GetByIdAsync(id, ct)
?? throw new NotFoundException("Task", id);
}
// ❌ Sync-over-async — deadlock risk
var task = _storage.GetByIdAsync(id).Result;
Always accept CancellationToken on async methods.
ConfigureAwait(false) in library code only.
Dependency Injection
Constructor injection — no property or method injection:
public class TaskService
{
private readonly ITaskStorage _storage;
private readonly ILogger<TaskService> _logger;
public TaskService(ITaskStorage storage, ILogger<TaskService> logger)
{
_storage = storage;
_logger = logger;
}
}
Register in DI container — never new a service:
builder.Services.AddScoped<ITaskStorage, PostgresTaskStorage>();
builder.Services.AddScoped<TaskService>();
LINQ
Prefer method syntax for complex queries, query syntax for joins:
var active = tasks
.Where(t => t.IsActive)
.OrderByDescending(t => t.Priority)
.Select(t => new TaskSummary(t.Id, t.Title));
Never mutate collections during LINQ iteration.
Naming
- PascalCase for classes, methods, properties, events, namespaces.
- camelCase for parameters, local variables.
_camelCase for private fields (prefix underscore).
I prefix for interfaces: ITaskStorage.
Async suffix for async methods: GetByIdAsync.
Testing
xUnit + FluentAssertions:
[Fact]
public async Task GetTask_ReturnsTask_WhenExists()
{
var result = await _service.GetTaskAsync("task-1", CancellationToken.None);
result.Should().NotBeNull();
result.Title.Should().Be("Test Task");
}
[Theory] for parameterized tests:
[Theory]
[InlineData("low", 1)]
[InlineData("medium", 5)]
[InlineData("high", 10)]
public void PriorityScore_MapsCorrectly(string priority, int expected)
{
Priority.Score(priority).Should().Be(expected);
}
NSubstitute or Moq for mocking.
Formatting and Static Analysis
| Tool |
Purpose |
Command |
dotnet format |
Canonical formatting |
dotnet format |
| Roslyn Analyzers |
Compile-time analysis |
Built-in |
SonarAnalyzer |
Comprehensive analysis |
NuGet package |
dotnet-outdated |
Dependency freshness |
dotnet-outdated |
dotnet list package --vulnerable |
CVE scanning |
Built-in (.NET 8+) |
Related
- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions
- Testing Strategy GEMINI.md § Testing Strategy
- Error Handling Principles GEMINI.md § Error Handling Principles
- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md
1---2name: csharp-idioms3description: C# Idioms and Patterns4---56## C# Idioms and Patterns78C# rewards type safety, LINQ expressiveness, and async-first design. Modern C# (10+/.NET 6+) favors records, nullable reference types, and minimal APIs. Idiomatic C# = clean, async-aware, framework-integrated.910> Scope: C# coding idioms. Test naming: GEMINI.md § Testing Strategy. Logging: `@.gemini/skills/logging-and-observability-principles/SKILL.md`.1112### Modern C# Features (10+)13141. **Nullable reference types — always enabled:**15 ```csharp16 // ✅ Explicit nullability17 public Task? FindById(string id) { ... }18 public Task GetById(string id) { ... } // never returns null — throws1920 // In .csproj: <Nullable>enable</Nullable>21 ```22232. **Records for immutable data:**24 ```csharp25 public record CreateTaskRequest(string Title, Priority Priority);26 public record TaskResponse(string Id, string Title, DateTime CreatedAt);27 ```28293. **Pattern matching:**30 ```csharp31 return result switch32 {33 Success(var task) => Ok(task),34 NotFound(var id) => NotFound($"Task {id} not found"),35 ValidationError(var errors) => BadRequest(errors),36 _ => StatusCode(500)37 };38 ```39404. **`required` and `init` for safe construction:**41 ```csharp42 public class AppConfig43 {44 public required string DatabaseUrl { get; init; }45 public required string ApiKey { get; init; }46 public int MaxRetries { get; init; } = 3;47 }48 ```4950### Error Handling51521. **Result pattern over exceptions for expected failures:**53 ```csharp54 public record Result<T>55 {56 public T? Value { get; init; }57 public string? Error { get; init; }58 public bool IsSuccess => Error is null;59 public static Result<T> Ok(T value) => new() { Value = value };60 public static Result<T> Fail(string error) => new() { Error = error };61 }62 ```63642. **Domain exceptions for unexpected failures — never raw `Exception`.**65663. **Never `catch (Exception)` without re-throw or specific handling.**6768### Async/Await69701. **Async all the way — never `.Result` or `.Wait()` on tasks:**71 ```csharp72 // ✅ Async pipeline73 public async Task<Task> GetTaskAsync(string id, CancellationToken ct)74 {75 return await _storage.GetByIdAsync(id, ct)76 ?? throw new NotFoundException("Task", id);77 }7879 // ❌ Sync-over-async — deadlock risk80 var task = _storage.GetByIdAsync(id).Result;81 ```82832. **Always accept `CancellationToken`** on async methods.84853. **`ConfigureAwait(false)`** in library code only.8687### Dependency Injection88891. **Constructor injection — no property or method injection:**90 ```csharp91 public class TaskService92 {93 private readonly ITaskStorage _storage;94 private readonly ILogger<TaskService> _logger;9596 public TaskService(ITaskStorage storage, ILogger<TaskService> logger)97 {98 _storage = storage;99 _logger = logger;100 }101 }102 ```1031042. **Register in DI container — never `new` a service:**105 ```csharp106 builder.Services.AddScoped<ITaskStorage, PostgresTaskStorage>();107 builder.Services.AddScoped<TaskService>();108 ```109110### LINQ1111121. **Prefer method syntax for complex queries, query syntax for joins:**113 ```csharp114 var active = tasks115 .Where(t => t.IsActive)116 .OrderByDescending(t => t.Priority)117 .Select(t => new TaskSummary(t.Id, t.Title));118 ```1191202. **Never mutate collections during LINQ iteration.**121122### Naming1231241. **PascalCase** for classes, methods, properties, events, namespaces.1252. **camelCase** for parameters, local variables.1263. **`_camelCase`** for private fields (prefix underscore).1274. **`I` prefix** for interfaces: `ITaskStorage`.1285. **`Async` suffix** for async methods: `GetByIdAsync`.129130### Testing1311321. **xUnit + FluentAssertions:**133 ```csharp134 [Fact]135 public async Task GetTask_ReturnsTask_WhenExists()136 {137 var result = await _service.GetTaskAsync("task-1", CancellationToken.None);138 result.Should().NotBeNull();139 result.Title.Should().Be("Test Task");140 }141 ```1421432. **`[Theory]` for parameterized tests:**144 ```csharp145 [Theory]146 [InlineData("low", 1)]147 [InlineData("medium", 5)]148 [InlineData("high", 10)]149 public void PriorityScore_MapsCorrectly(string priority, int expected)150 {151 Priority.Score(priority).Should().Be(expected);152 }153 ```1541553. **NSubstitute or Moq for mocking.**156157### Formatting and Static Analysis158159| Tool | Purpose | Command |160|---|---|---|161| `dotnet format` | Canonical formatting | `dotnet format` |162| Roslyn Analyzers | Compile-time analysis | Built-in |163| `SonarAnalyzer` | Comprehensive analysis | NuGet package |164| `dotnet-outdated` | Dependency freshness | `dotnet-outdated` |165| `dotnet list package --vulnerable` | CVE scanning | Built-in (.NET 8+) |166167### Related168- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions169- Testing Strategy GEMINI.md § Testing Strategy170- Error Handling Principles GEMINI.md § Error Handling Principles171- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md