# Server C# conventions

Style, types, naming, async, error handling and tests. The successor to `server/CONVENTIONS.md`.

> Last verified: 2026-07-30 against commit `d3ec723`.

When in doubt, ask: *would a senior engineer approve this diff without comment?*

---

## 1. Use the right type for the job

| Scenario | Use |
| --- | --- |
| Request / response / DTO | `record` — immutable, value semantics |
| Domain entity | `class` — mutable state, **encapsulated** |
| Shared small value | `readonly record struct` |
| Handler, service | `sealed class` |

### Immutability and safety

- Mark fields `readonly` unless mutation is genuinely needed.
- Prefer `IReadOnlyList<T>` / `IReadOnlyCollection<T>` in signatures unless the caller must mutate.
- **Never expose a public setter on an entity.** Use methods or the constructor. A lifecycle `status` gets a
  private setter and cohesive transition methods — see [persistence.md](persistence.md) §5.
- Avoid `static` mutable state.

### Null handling

- `<Nullable>enable</Nullable>` in any new project.
- Guard clauses at the entry point; don't scatter null checks through a method.
- Prefer `OperationResult.NotFoundResult(...)` over returning `null` from a handler.
- **Never `null!`** unless you can prove the value cannot be null and the compiler cannot.

### Use the language

```csharp
// primary constructor (C# 12)
public sealed class OrderHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<…> { }

// switch expression over an if/else chain
var label = status switch
{
    OrderStatus.Pending   => "Pending",
    OrderStatus.Shipped   => "Shipped",
    OrderStatus.Cancelled => "Cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status)),
};

// pattern matching
if (result is { IsSuccess: false, IsNotFound: true }) return NotFound();

// collection expressions (C# 12)
List<string> tags = ["new", "sale"];
```

---

## 2. Naming

| Kind | Convention | Example |
| --- | --- | --- |
| Class, record, interface | PascalCase | `OrderHandler`, `IOrderRepository` |
| Method | PascalCase | `GetUserOrdersAsync` |
| Parameter, local | camelCase | `orderId`, `userEmail` |
| Private field | `_camelCase` | `_unitOfWork` |
| Constant | PascalCase | `MaxRetryCount` |
| Generic type parameter | `T`, or descriptive `TEntity` | |
| Command | `{Verb}{Noun}Command` | `CreateOrderCommand` |
| Query | `{Verb}{Noun}Query` | `GetUserOrdersQuery` |
| Handler | `{RequestName}Handler` | `CreateOrderCommandHandler` |
| Result DTO | `{RequestName}Result` | `CreateOrderCommandResult` |

No abbreviations unless universally understood (`dto`, `id`, `url`). No Hungarian notation (`strName`,
`intCount`).

The `Baya.*` prefix is project naming, not the brand — see [shared/naming.md](../shared/naming.md).

---

## 3. Routing

All URL segments are `snake_case`. `SnakeCaseParameterTransformer` (`Baya.WebFramework/Routing/`) is
registered globally via `RouteTokenTransformerConvention` and converts `[controller]` and `[action]` tokens
automatically.

```csharp
// ✅ the transformer converts MyFeature → my_feature, GetBySlug → get_by_slug
[Route("api/v{version:apiVersion}/[controller]")]
public sealed class MyFeatureController : BaseController
{
    [HttpGet("[action]")]
    public Task<IActionResult> GetBySlug(…) { }
}

// ❌ hardcoded segments bypass the transformer and escape snake_case enforcement
[Route("api/v{version:apiVersion}/MyFeature")]
[HttpGet("GetBySlug")]
```

**If a method name doesn't read cleanly as a URL, rename the method.** Don't hardcode the route string — it
also breaks the dynamic-permission key, which is derived from the same route values.

The controller skeleton and authorization table are in [cqrs.md](cqrs.md) §4.

---

## 4. Async / await

```csharp
// ✅ async all the way — no .Result, no .Wait()
public async ValueTask<OperationResult<T>> Handle(MyQuery request, CancellationToken ct)
{
    var entity = await _repository.GetAsync(request.Id, ct);
    return OperationResult<T>.SuccessResult(_mapper.Map(entity));
}

// ❌ blocks the thread, risks deadlock
var result = _repository.GetAsync(id).Result;

// ❌ fire and forget with no error handling
_ = DoSomethingAsync();
```

- **Every public async method accepts a `CancellationToken` and passes it downstream** — including into
  `SaveChangesAsync(ct)` and `sender.Send(command, ct)`.
- Use **`ValueTask<T>`** for hot paths (handlers, repositories); `Task<T>` for rarely-called or always-async
  methods.
- **Never `async void`** — it swallows exceptions. Use `async Task` even for an event-like callback.
- **Do not add `.ConfigureAwait(false)`** in this ASP.NET Core app. It is unnecessary here and adds noise.

---

## 5. Error handling and logging

```csharp
// ✅ expected failure — return, don't throw
if (user is null)
    return OperationResult<T>.NotFoundResult("User not found.");

// ❌ swallowing an exception into a generic failure
try { … } catch { return OperationResult<T>.FailureResult(…); }
```

The global `ExceptionHandler` middleware catches unhandled exceptions and logs them. **Do not add a try/catch
for unknown exceptions in a handler** — let them propagate. Catch only what you can actually handle.

Logging rules are in [identity.md](identity.md) §9: structured templates, no PII or secrets, correct level.

---

## 6. Validation

- Every command that accepts user input needs a FluentValidation validator. `ValidateCommandBehavior` runs it
  automatically before the handler, and `RegisterValidatorsAsServices()` registers them.
- **Validate at the boundary** — the command or query — not deep in the domain or a repository.
- **Never validate a route-supplied id in the body command.** See [cqrs.md](cqrs.md) §3.

---

## 7. Mapping — Mapster

- Use the injected `IMapper` for entity↔DTO mapping **in handlers**.
- Register type-adapter configs in `Program.cs` via `TypeAdapterConfig.GlobalSettings.Scan(...)`; add new
  assemblies containing mapping configs there.
- Never write manual mapping code where Mapster can infer it. Only write a custom `TypeAdapterConfig` when
  shapes genuinely diverge.
- **Mapping happens in the handler after the DB query**, never in the repository — the repository projects.

---

## 8. Testing

### Arrange — Act — Assert, always

```csharp
[Fact]
public async Task CreateOrder_ValidCommand_ReturnsSuccess()
{
    // Arrange
    var command = new CreateOrderCommand(UserId: 1, Items: [new(ProductId: 5, Quantity: 2)]);
    var handler = new CreateOrderCommandHandler(_unitOfWork, _mapper);

    // Act
    var result = await handler.Handle(command, CancellationToken.None);

    // Assert
    result.IsSuccess.Should().BeTrue();
    result.Result.Should().NotBeNull();
}
```

- **Test the handler directly**, not the controller — controllers are thin wrappers.
- **`NSubstitute`** for mocking: `Substitute.For<IUnitOfWork>()`.
- **Persistence tests use the in-memory SQLite context** from `Baya.Tests.Setup` rather than mocking the DB.
- Name tests `{MethodUnderTest}_{Scenario}_{ExpectedOutcome}`.
- One assertion *concept* per test. Multiple `.Should()` calls are fine if they verify the same outcome.
- **Don't test EF internals** (tracking, migrations) — test behaviour through the handler.

### Integration tests — the HTTP pipeline

Handler tests leave the whole HTTP stack untested: routing, the auth pipeline, middleware, and the
`OperationResult → IActionResult` translation. **Each feature area needs at least one
`WebApplicationFactory<Program>` test** in `Baya.Test.Api` (environment `Testing`, in-memory SQLite) covering:

1. **Happy path** — an authenticated request returns 200 with the right body shape.
2. **Unauthenticated** — returns 401.
3. **Validation failure** — returns 400 with field-level error detail.

```csharp
public class MyFeatureApiTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetSomething_Authenticated_Returns200()
    {
        var client = factory.CreateClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", TestTokens.ValidAdminToken);

        var response = await client.GetAsync("/api/v1/my_feature/get_something");

        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}
```

The recurring-job scheduler is **dormant under `Testing`**, so a background tick can't make an integration
test flaky.

---

## 9. Service registration

- Every new infrastructure service gets an extension method in that project's `ServiceConfiguration/` folder,
  called from `Program.cs`. **No inline DI registration in `Program.cs`.**
- Lifetimes: **Singleton** for stateless, thread-safe services (`IHttpContextAccessor`, `IFieldEncryptor` —
  which *must* be a singleton, see [identity.md](identity.md) §3); **Scoped** for per-request services
  (repositories, `DbContext`, handlers); **Transient** for lightweight stateless ones (validators,
  transformers).
- **All NuGet versions live only in `Directory.Packages.props`.** Never add `Version=` to a
  `<PackageReference>` in a `.csproj`.

---

## 10. Code organisation

- **One type per file**, file name matching the type name exactly.
- Handlers and validators live in the **same feature folder** — not in a root `Handlers/` or `Validators/`.
- A file over **~150 lines** usually means mixed concerns. Consider splitting it.
- **Partial classes are only for generated code** (source generators, EF scaffolding) — and the one deliberate
  exception, `DemoLifecycleSeeder`'s `.Money.cs`/`.Social.cs` partials, which split a Development-only seeder
  by domain.
- **`Program.cs` stays an orchestrator** — extension-method calls only, no logic.

---

## 11. No unused code, and comment the *why*

Both are shared rules with real teeth on this side: the gate is **zero new warnings**, and `CS0168` / `CS0219`
/ `CS0169` / `IDE0005` all surface dead code. **Delete it — don't `#pragma warning disable` it.**

The one exception: a parameter that must exist to satisfy an interface or delegate signature but is genuinely
unused. Keep it, name it conventionally, and add a one-line `// why` only if the reason isn't obvious.

Full rules, with examples of a comment that earns its place: [shared/code-quality.md](../shared/code-quality.md).

Known pre-existing warnings that must **not** be fixed unless a task says so:
[shared/git-and-gates.md](../shared/git-and-gates.md) §5.
