# C# Coding Standards

> **Scope:** universal
> **Layer:** 0 (always load)
> **Keywords:** coding, naming, async, style, conventions, csharp
> **Load When:** always

Single source of truth for C# naming conventions, code style, and enforcement.
Based on [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) and [.NET Naming Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines).

---

## Naming Conventions

| Element | Convention | Example |
|---------|-----------|---------|
| Classes | PascalCase | `OrderService` |
| Interfaces | `I` + PascalCase | `IOrderService` |
| Methods | PascalCase | `GetOrderAsync` |
| Async methods | + `Async` suffix | `ProcessPaymentAsync` |
| Properties | PascalCase | `OrderTotal` |
| Constants | PascalCase | `DefaultPageSize` **NOT** `DEFAULT_PAGE_SIZE` |
| Enums / Members | PascalCase | `OrderStatus.PendingPayment` **NOT** `PENDING_PAYMENT` |
| Namespaces | PascalCase | `MyApp.Domain` |
| Type parameters | `T` + PascalCase | `TEntity`, `TResult` |
| Private fields | `_camelCase` | `_orderRepository` |
| Static private | `s_camelCase` | `s_instance` |
| Thread-static | `t_camelCase` | `t_contextId` |
| Locals / Params | camelCase | `orderId`, `orderTotal` |

### Acronyms

| Length | Rule | Example |
|--------|------|---------|
| 2 chars | Both uppercase | `IO`, `UI` |
| 3+ chars | PascalCase | `Html`, `Xml`, `Http` |

### DON'T

| Wrong | Right |
|-------|-------|
| `strName`, `iCount` | `name`, `count` |
| `MAX_RETRY_COUNT` | `MaxRetryCount` |
| `btnSubmit`, `lblName` | `submitButton`, `nameLabel` |
| `order_total`, `get_order` | `orderTotal`, `GetOrder` |
| `repo`, `ctx`, `mgr`, `svc` | `repository`, `context`, `manager`, `service` |
| `Int32 count`, `String name` | `int count`, `string name` |

---

## Code Style

### Modern C# (10+)

```csharp
namespace MyApp.Application.Services;  // File-scoped

public sealed class OrderService(IOrderRepository repository) : IOrderService
{
    public async Task<Result<Order>> GetByIdAsync(int id, CancellationToken ct = default)
    {
        var order = await repository.GetByIdAsync(id, ct);
        return order is null
            ? Result.Failure<Order>("Not found")
            : Result.Success(order);
    }
}
```

### Style Rules

| Rule | Wrong | Right |
|------|-------|-------|
| `var` when obvious | `OrderDto order = new OrderDto()` | `var order = new OrderDto()` |
| Target-typed `new` | `OrderDto order = new OrderDto()` | `OrderDto order = new()` |
| Null checks | `== null`, `!= null` | `is null`, `is not null` |
| Collections | `new List<int> { 1, 2 }` | `List<int> items = [1, 2]` |
| Expression body | `{ return _name; }` | `=> _name;` |
| Nullable refs | Disabled | `<Nullable>enable</Nullable>` |
| Pattern matching | `if (x is int)` then cast | `if (x is int value)` |
| Default `sealed` | Unsealed classes | `sealed` unless inheritance needed |

### Complexity Thresholds

| Metric | Limit |
|--------|-------|
| Method length | < 30 lines (prefer < 20) |
| Method parameters | ≤ 4 (use record for more) |
| Class length | < 300 lines |
| Nesting depth | ≤ 3 levels |
| Cyclomatic complexity | < 10 per method |

---

## Async Conventions

```csharp
// ALWAYS: Async suffix + CancellationToken
public async Task<Order> GetOrderAsync(int id, CancellationToken ct = default)

// NEVER: .Result or .Wait() (deadlock risk in Blazor Server)
var order = await service.GetOrderAsync(id, ct);  // Correct
var order = service.GetOrderAsync(id).Result;     // DEADLOCK

// NEVER: async void (except event handlers)
public async Task HandleClickAsync() { }  // Correct
public async void HandleClick() { }       // exceptions lost
```

---

## Dependency Injection

| Lifetime | When | Example |
|----------|------|---------|
| **Scoped** | DbContext, per-request | `AddScoped<IOrderService, OrderService>()` |
| **Singleton** | Factories, caches | `AddSingleton<IRepositoryFactory>()` |
| **Transient** | Lightweight, stateless | `AddTransient<IValidator<Order>>()` |

```csharp
// Constructor injection with interfaces
public sealed class OrderService(IOrderRepository repository, ILogger<OrderService> logger)

// Service Locator anti-pattern
var service = serviceProvider.GetService<IOrderService>();
```

---

## Logging

```csharp
// Structured logging with message templates
logger.LogInformation("Processing order {OrderId} for {CustomerId}", orderId, customerId);

// String interpolation defeats structured logging
logger.LogInformation($"Processing order {orderId}");  // WRONG

// Include correlation IDs
logger.LogError(ex, "Failed to process order {OrderId}", orderId);
```

| Level | When |
|-------|------|
| `Trace` | Fine-grained diagnostic (dev only) |
| `Debug` | Internal state, troubleshooting |
| `Information` | Normal operations (start, complete, key decisions) |
| `Warning` | Unexpected but recoverable (retry, fallback) |
| `Error` | Failure requiring attention |
| `Critical` | System-level failure (app crash, data corruption) |

---

## Error Handling

```csharp
// Result pattern for business errors
public async Task<Result<Order>> ProcessAsync(Guid id, CancellationToken ct)
{
    var order = await _repository.GetByIdAsync(id, ct);
    if (order is null) return Result.Failure<Order>("Not found");
    if (order.Status >= OrderStatus.Completed)
        return Result.Failure<Order>("Already completed");
    return Result.Success(order);
}

// Exceptions for infrastructure errors only
// NEVER: empty catch or swallowed exceptions
try { await _client.SendAsync(request, ct); }
catch (Exception) { }  // WRONG

// ALWAYS: preserve inner exception
catch (HttpRequestException ex)
{
    throw new PaymentGatewayException("Payment failed", ex);
}

// ALWAYS: specific exception types
catch (DbUpdateConcurrencyException ex) { /* handle */ }
catch (Exception ex) { /* log and rethrow */ throw; }
```

---

## .editorconfig Template

Minimal `.editorconfig` for enforcement:

```ini
[*.cs]
# Naming
dotnet_naming_rule.interfaces.severity = error
dotnet_naming_rule.interfaces.symbols = interface
dotnet_naming_rule.interfaces.style = begins_with_i

# File-scoped namespaces
csharp_style_namespace_declarations = file_scoped:warning

# var preferences
csharp_style_var_when_type_is_apparent = true:suggestion

# Null checks
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning

# Async suffix
dotnet_naming_rule.async_methods.severity = warning
dotnet_naming_rule.async_methods.required_suffix = Async
```

Full template: `.editorconfig` in project root.

---

*MORPH-SPEC by Polymorphism Tech*
