Files
Hy2026/backend/AGENTS.md
T
Rafał Tyński f4f3b3896a docs(backend): document C# conventions in AGENTS.md
Add C# language conventions: one type per file, file-scoped namespaces, sealed/internal types, DTO as records, ban on object-valued dictionaries with the enforcing test, layering, naming and formatting rules.
2026-10-04 03:49:39 +02:00

5.7 KiB

AGENTS.md — Założenia i standardy backendu (C#)

Dotyczy projektu backend oraz testów tests/TarninaHub.Tests (ASP.NET Core Web API, .NET 10).

Komunikaty commitów

  • Komunikaty commitów zapisujemy wyłącznie w języku angielskim.
  • Zasada obowiązuje w obu projektach: repozytorium DevHeyah oraz 20_projects/TarninaHub.
  • Stosujemy konwencję Conventional Commits (typ: feat, fix, docs, chore itd.).

Założenia języka C#

  • Platforma: .NET 10 (net10.0), Nullable enable, ImplicitUsings enable. Nie wyłączaj nullowalności; unikaj operatora ! (jeśli musisz — uzasadnij).
  • Jeden typ = jeden plik: każdy typ najwyższego poziomu (klasa, record, interfejs, enum, struct) w osobnym pliku, nazwa pliku zgodna z nazwą typu. Nie zagnieżdżaj typów pomocniczych poza przypadkami prywatnych szczegółów implementacji.
  • Przestrzeń nazw: wyłącznie file-scoped (namespace X;). Kolejność: using → namespace → typy. Grupuj using bez zbędnych duplikatów.
  • Typy: klasy usług i kontrolerów oznaczaj sealed; modele wierszy i typy wewnętrzne jako internal. DTO to public sealed record (niemutowalne, pozycyjne).
  • Zakaz map z wartościami object: w DTO, interfejsach, Domain i modelach *Row nie stosuj Dictionary<string, object> ani IReadOnlyDictionary<string, object>. Elastyczne dane (content, payload, contact, media, attachments) to mapy wartości tekstowych (IReadOnlyDictionary<string, string?>), materializowane do konkretnych Dictionary<string, string?> przed przekazaniem do RawQuery. Powód: błąd deserializacji CBOR (Dahomey) i nieobsługiwana serializacja typów interfejsowych. Egzekwuje test NoObjectValuedDictionariesTests.
  • Wzorce: promuj record, pattern matching, wyrażenia (=>), kolekcje []; unikaj null-owych kolekcji (inicjalizuj puste).
  • Warstwy: Domain (reguły, enumy), Application (DTO, abstrakcje, mapowania), Infrastructure (persistence, integracje), Controllers (tylko HTTP). Zależności kieruj do wewnątrz (Infrastructure → Application → Domain).
  • Nazewnictwo: typy/metody/właściwości PascalCase, parametry i zmienne lokalne camelCase, pola prywatne _camelCase, interfejsy I..., typy generyczne T.... Metody asynchroniczne z sufiksem Async.
  • Formatowanie: zgodnie z .editorconfig (C#: 4 spacje, LF, final newline, bez trailing whitespace). Weryfikacja: dotnet format whitespace --verify-no-changes TarninaHub.slnx i dotnet format style --verify-no-changes --severity warn TarninaHub.slnx.
  • Zmienne: preferuj var, gdy typ jest oczywisty; jawne typy przy niejasności.

Język nazewnictwa

  • Wszystkie nazwy zmiennych, parametrów, pól, metod, klas i plików są wyłącznie w języku angielskim.
  • Zakaz używania polskich nazw w kodzie (np. uzytkownik, zamowienie), w tym w parametrach tras i modelach.
  • Komentarze i dokumentacja opisowa mogą być w języku polskim, ale identyfikatory w kodzie — tylko po angielsku.

Organizacja kontrolerów

  • Każdy kontroler w osobnym pliku w katalogu Controllers/, nazwa pliku zgodna z nazwą klasy.
  • Nazwa klasy: <Resource>Controller (np. CustomersController), dziedziczy po ControllerBase.
  • Stosuj atrybut [ApiController] oraz jawne trasy [Route("api/<resource>")] (np. [Route("api/health")]) — nie używaj tokenu [controller].
  • Katalog Controllers/ grupuje wyłącznie kontrolery — logika biznesowa trafia do serwisów.

Konwencje akcji

  • Metody akcji w czasie teraźniejszym, opisujące operację: GetById, GetAll, Create, Update, Delete.
  • Używaj atrybutów HTTP: [HttpGet], [HttpGet("{id}")], [HttpPost], [HttpPut("{id}")], [HttpDelete("{id}")].
  • Parametry tras w {camelCase} (np. {customerId}), zgodnie z nazwami w kodzie.
  • Zwracaj typy wyników ActionResult<T> lub IActionResult z jawnymi kodami statusu:
    • 200 OK — pobranie/zaktualizowanie,
    • 201 Created — utworzenie (CreatedAtAction),
    • 204 NoContent — usunięcie,
    • 400 BadRequest, 404 NotFound — błędy.
  • Kontroler nie zawiera logiki biznesowej ani dostępu do bazy — deleguje do serwisów/repozytoriów.
  • Używaj wstrzykiwania zależności przez konstruktor (tylko readonly pola).

Modele i DTO

  • Wejście i wyjście kontrolera stanowią dedykowane DTO, nie modele encji.
  • Nazwy DTO: <Resource>Request, <Resource>Response.
  • Walidacja wejścia przez adnotacje ([Required], [StringLength], [Range]).
  • Nie zwracaj encji bazy bezpośrednio z akcji.

Asynchroniczność

  • Wszystkie operacje I/O (baza, HTTP) jako async z sufiksem Async i zwrotem Task<...>.
  • Akceptuj CancellationToken cancellationToken i przekazuj go dalej.

Przykład

[ApiController]
[Route("api/customers")]
public class CustomersController : ControllerBase
{
    private readonly ICustomerService _customerService;

    public CustomersController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [HttpGet("{customerId:int}")]
    public async Task<ActionResult<CustomerResponse>> GetById(
        int customerId,
        CancellationToken cancellationToken)
    {
        var customer = await _customerService.GetByIdAsync(customerId, cancellationToken);

        return customer is null
            ? NotFound()
            : Ok(customer);
    }

    [HttpPost]
    public async Task<ActionResult<CustomerResponse>> Create(
        CreateCustomerRequest request,
        CancellationToken cancellationToken)
    {
        var createdCustomer = await _customerService.CreateAsync(request, cancellationToken);

        return CreatedAtAction(
            nameof(GetById),
            new { customerId = createdCustomer.Id },
            createdCustomer);
    }
}