Files
2026-05-19 11:27:12 +02:00

395 lines
15 KiB
Markdown

# Safe Code - Zasady bezpiecznego kodu biznesowego Soneta
Lista zasad, które należy weryfikować **po każdym refaktoringu** oraz **przy każdym code review / PR** kodu biznesowego enova365 / Soneta Enterprise. Każda zasada jest sformułowana jako kontrolne pytanie — odpowiedź "nie" oznacza realne ryzyko (utrata danych, niespójność, race condition, błędna logika).
## Spis treści
- [1. Sesje i transakcje](#1-sesje-i-transakcje)
- [2. Mieszanie sesji i obiektów](#2-mieszanie-sesji-i-obiektów)
- [3. Thread-safety](#3-thread-safety)
- [4. Optimistic locking i konflikty](#4-optimistic-locking-i-konflikty)
- [5. Walidacja danych](#5-walidacja-danych)
- [6. Filtrowanie po stronie serwera](#6-filtrowanie-po-stronie-serwera)
- [7. Kod biznesowy vs UI](#7-kod-biznesowy-vs-ui)
- [8. ExecuteConfig - dane konfiguracyjne](#8-executeconfig---dane-konfiguracyjne)
- [9. Obsługa wyjątków i rollback](#9-obsługa-wyjątków-i-rollback)
- [10. Typy biznesowe](#10-typy-biznesowe)
- [11. Tłumaczenia i komunikaty](#11-tłumaczenia-i-komunikaty)
- [12. Logowanie](#12-logowanie)
- [13. Wydajność jako bezpieczeństwo](#13-wydajność-jako-bezpieczeństwo)
- [14. Czystość API publicznego](#14-czystość-api-publicznego)
- [15. Code review checklist (TL;DR)](#15-code-review-checklist-tldr)
---
## 1. Sesje i transakcje
### 1.1 Każda sesja w `using`
Sesja implementuje `IDisposable` — bez `using` zostanie ona w pamięci do GC, blokując zasoby i potencjalnie powodując nieskończony wzrost zużycia pamięci.
```csharp
// ŹLE
var session = login.CreateSession(readOnly: false, config: false, name: "Op");
// ... brak Dispose
// DOBRZE
using (var session = login.CreateSession(readOnly: false, config: false, name: "Op")) {
// ...
}
```
---
## 2. Mieszanie sesji i obiektów
### 2.1 Nigdy nie używaj `Row` z innej sesji bezpośrednio
Każdy obiekt biznesowy żyje tylko w swojej sesji. Przekazanie go do innej sesji powoduje nieprzewidywalne zachowanie (stary stan, niespójne klucze).
```csharp
// ŹLE
faktura.Kontrahent = kontrahentZInnejSesji;
// DOBRZE
faktura.Kontrahent = session.Get(kontrahentZInnejSesji);
```
### 2.2 Nie zwracaj obiektów sesyjnych poza `using`
```csharp
// ŹLE - Towar po wyjściu z using jest "martwy"
public Towar PobierzTowar(Login login, string kod) {
using (var session = login.CreateSession(true, false, "Pobierz")) {
return session.GetTowary().Towary.WgKodu[kod];
}
}
```
Zwracaj DTO / prymitywy, albo niech wywołujący kontroluje cykl życia sesji.
---
## 3. Thread-safety
### 3.1 Lista współdzielonych vs niewspółdzielonych
| Można używać jednocześnie w wielu wątkach | Nie wolno używać jednocześnie w wielu wątkach |
|-------------------------------------------|------------------------------------------------|
| `BusApplication`, `Database`, `Login` | `Session`, `Module`, `Table`, `Row`, `Context` |
### 3.2 Singleton / statyczne pola - bez sesyjnych referencji
Statyczne pole trzymające `Session`, `Row`, `Table` to klasyczna pułapka — pierwsza sesja "zawłaszcza" pole i kolejne wątki widzą cudze dane.
---
## 4. Optimistic locking i konflikty
### 4.1 Konflikty wykrywane w `Save()`
Soneta używa optimistic concurrency. Konflikt edycji (ktoś inny zapisał ten sam rekord) wybucha jako wyjątek z `session.Save()`. Jeśli kod łapie generyczny `Exception` i kontynuuje, użytkownik dostaje "zapisano" mimo że dane nie poszły do bazy.
```csharp
try {
session.Save();
}
catch (RowConflictException ex) {
// świadoma obsługa: refresh + powtórzenie lub eskalacja do użytkownika
}
```
### 4.2 Nie ignoruj wyjątku z `Save()`
`Save()` zgłaszający wyjątek = **nie zapisano**. Każdy taki przypadek musi mieć dedykowaną obsługę (retry, log, komunikat). Połknięcie wyjątku = utrata danych.
---
## 5. Walidacja danych
### 5.1 Walidacja przed `transaction.Commit()`
Wyjątek po `Commit()` ale przed `Save()` nie wycofuje zmian z bieżącej sesji — tylko z bazy. Następne `Save()` w tej samej sesji może je przepuścić niezauważenie. Walidacja musi rzucić wyjątek **przed** `Commit()` lub w event-handlerze `SavingRow`.
### 5.2 Komunikaty walidacyjne przez `Translate`
```csharp
throw new RowException(this, "Pole jest wymagane".Translate());
throw new RowException(this, "Pole {0} jest wymagane".TranslateFormat(nameof(Nazwa)));
```
---
## 6. Filtrowanie po stronie serwera
### 6.1 Nie iteruj całej tabeli z `if` w pamięci
```csharp
// ŹLE - ściąga całą tabelę
foreach (Towar t in tm.Towary.WgKodu) {
if (t.Aktywny && t.Cena > 100) { ... }
}
// DOBRZE - warunek wykonany przez serwer
foreach (Towar t in tm.Towary[(Towar t) => t.Aktywny && t.Cena > 100]) { ... }
```
Szczegóły wyrażeń `RowCondition` (LINQ-like, predykaty, kompozycja) — patrz [rowcondition.md](rowcondition.md).
### 6.2 Nie używaj `View` w kodzie biznesowym
`View` należy do warstwy UI. W kodzie biznesowym używaj `SubTable[condition]`. Szczegóły — patrz [viewinfo.md](viewinfo.md).
### 6.3 Nie wczytuj całych tabel kartotekowych ani operacyjnych
Tabele dzielą się na trzy kategorie pod kątem rozmiaru i bezpieczeństwa pełnego skanu:
| Kategoria | Przykłady | Pełny skan? |
|-----------|-----------|-------------|
| **Konfiguracyjne** (stałe, niewielkie) | `Jednostki`, `DefinicjeDokumentow`, `Stawki VAT` | **Dozwolony** |
| **Kartotekowe** (rosną z biznesem) | `Towary`, `Kontrahenci`, `Pracownicy` | **Zabroniony** bez filtra |
| **Operacyjne** (rosną w czasie, guided) | `DokHandlowe`, `Zapisy`, `WyplatyElementy` | **Zabroniony** — wymagany zakres czasowy |
**Reguła dla tabel operacyjnych guided** (zawierających daty): dostęp prawie zawsze musi być ograniczony do określonego okresu.
```csharp
// ŹLE - pobiera wszystkie dokumenty od początku istnienia bazy
foreach (DokumentHandlowy d in hm.DokHandlowe.WgNumeru) { ... }
// DOBRZE - zakres czasowy
var od = new Date(2026, 1, 1);
var doD = Date.Today;
foreach (DokumentHandlowy d in hm.DokHandlowe[(DokumentHandlowy d) => d.Data >= od && d.Data <= doD]) { ... }
```
**Reguła dla danych operacyjnych nie-guided** (pozycje, składniki, zapisy szczegółowe): używaj ich w zakresie obiektu guided root (np. pozycje konkretnego dokumentu, składniki konkretnej wypłaty). Jeśli musisz iterować poprzecznie, **dodaj filtr czasowy analogiczny do tabel guided**.
```csharp
// DOBRZE - w zakresie roota
foreach (PozycjaDokHandlowego p in faktura.Pozycje) { ... }
// ŹLE - iteracja po wszystkich pozycjach historycznych
foreach (PozycjaDokHandlowego p in hm.PozycjeDokHan.WgDokument) { ... }
// AKCEPTOWALNE - poprzeczna iteracja z filtrem czasowym (przez root)
var od = new Date(2026, 1, 1);
foreach (DokumentHandlowy d in hm.DokHandlowe.WgDaty[d => d.Data >= od]) {
foreach (var p in d.Pozycje) { ... }
}
```
---
## 7. Kod biznesowy vs UI
### 7.1 Brak referencji do UI w kodzie biznesowym
Kod, który będzie wywoływany z workerów, schedulera, importu, API — **nie może** używać:
- `View`, `ViewInfo` (zamiast tego: `SubTable[condition]`)
- `IsReadOnlyXxx`, `IsVisibleXxx`, `IsEnabledXxx`, `GetListXxx`, `GetAppearanceXxx`
- okien dialogowych, `MessageBox`, `IUIServices`
### 7.2 Nie sprawdzaj `AccessRight` w logice biznesowej
```csharp
// ŹLE - logika biznesowa nie ma znać uprawnień
if (Table.AccessRight == AccessRights.Denied) return;
```
Prawa dostępu są warstwą UI / autoryzacji. Logika biznesowa zakłada poprawne wywołanie; egzekucja praw jest gdzie indziej.
---
## 8. ExecuteConfig - dane konfiguracyjne
### 8.1 Nie zwracaj obiektów sesyjnych z `ExecuteConfig`
`ExecuteConfig` korzysta z sesji współdzielonej między wątkami. Obiekt zwrócony na zewnątrz może być w międzyczasie unieważniony.
```csharp
// ŹLE
return login.ExecuteConfig(s => s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]);
// DOBRZE - zwracamy prymityw / kopię
return login.ExecuteConfig(s => s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]?.Kategoria);
```
### 8.2 Nie używaj `Session.ConfigSession` — używaj `Session.ExecuteConfig()`
`Session.ConfigSession` daje bezpośredni uchwyt do sesji konfiguracyjnej, którego cykl życia nie jest kontrolowany przez wywołującego — łatwo o wyciek obiektów sesyjnych poza wątek docelowy lub o korzystanie z nieaktualnego stanu.
```csharp
// ŹLE
var def = session.ConfigSession.GetHandel().DefDokHandlowych.WgSymbolu["ZK"];
// DOBRZE
var kategoria = session.ExecuteConfig(s =>
s.GetHandel().DefDokHandlowych.WgSymbolu["ZK"]?.Kategoria);
```
`ExecuteConfig` ogranicza zasięg sesji konfiguracyjnej do bloku lambda i wymusza zwracanie prymitywów (patrz §8.1).
---
## 9. Obsługa wyjątków i rollback
### 9.1 Nie łap `Exception` bez konkretu
Generyczne `catch (Exception) { /* log */ }` ukrywa konflikty, błędy walidacji, brak praw, błędy I/O. Każdy z nich wymaga innej reakcji. Łap konkretne typy: `RowConflictException`, `RowException`, `BusException`, `LoginException`.
### 9.2 Nie rzucaj nieokreślonego `Exception`
`throw new Exception("...")` zmusza wywołującego do łapania `Exception` ogólnie, co (patrz §9.1) tłumi sygnały o realnej przyczynie błędu. Rzucaj **typowane** wyjątki, dobrane do warstwy:
| Sytuacja | Wyjątek |
|----------|---------|
| Naruszenie reguły biznesowej powiązane z rekordem | `RowException(row, "komunikat".Translate())` |
| Ogólny błąd logiki biznesowej (bez konkretnego wiersza) | `BusException` |
| Nieprawidłowy stan obiektu / nieprawidłowe wywołanie API | `InvalidOperationException` |
| Nieprawidłowy argument | `ArgumentException`, `ArgumentNullException`, `ArgumentOutOfRangeException` |
| Funkcja jeszcze nie obsłużona | `NotSupportedException`, `NotImplementedException` |
```csharp
// ŹLE
throw new Exception("Brak definicji dokumentu");
// DOBRZE - błąd biznesowy bez konkretnego wiersza
throw new BusException("Brak definicji dokumentu {0}".TranslateFormat(symbol));
// DOBRZE - błąd walidacyjny związany z wierszem
throw new RowException(faktura, "Pole {0} jest wymagane".Translate(), nameof(faktura.Kontrahent));
// DOBRZE - nieprawidłowy stan / niepoprawne wywołanie
throw new InvalidOperationException("Worker wymaga otwartej transakcji edycyjnej");
```
---
## 10. Typy biznesowe
### 10.1 Pieniądze, ilości, daty - dedykowane typy
| Pojęcie | Typ | NIE używaj |
|---------|-----|------------|
| Kwota walutowa | `Currency`, `DoubleCy` | `decimal`, `double` (gubi walutę) |
| Ilość z jednostką | `Quantity` | `decimal`, `int` (gubi jednostkę) |
| Data biznesowa | `Soneta.Types.Date` | `DateTime` (gubi semantykę "tylko data") |
| Procent | `Percent` | `double` |
### 10.2 `Date.Today` zamiast `DateTime.Today`
`Date.Today` honoruje datę biznesową aplikacji (np. zamknięty miesiąc). `DateTime.Today` zwraca datę systemową.
---
## 11. Tłumaczenia i komunikaty
### 11.1 Każdy łańcuch widoczny dla użytkownika przez `.Translate()`
```csharp
throw new RowException(this, "Pole jest wymagane".Translate());
throw new RowException(this, "Pole {0} jest wymagane".TranslateFormat(nameof(Nazwa)));
```
### 11.2 Stałe konfiguracyjne nie przez `Translate`
`Translate` jest dla tekstu UI / komunikatów. Klucze, symbole, kody nie powinny być tłumaczone — to dane techniczne.
### 11.3 `[TranslateIgnore]` dla pól nie do tłumaczenia
Szczegóły mechanizmów tłumaczeń — patrz [translations-logging.md](translations-logging.md).
---
## 12. Logowanie
### 12.1 Nie loguj danych wrażliwych (PII, hasła, tokeny)
PESEL, NIP w nadmiarze, hasła, klucze API nie trafiają do logów.
---
## 13. Wydajność jako bezpieczeństwo
Wolny kod = timeouty = nieskończone retry = blokady / data corruption.
### 13.1 Pętla po tabeli w transakcji edycyjnej - krótka
Długa transakcja blokuje innych użytkowników na poziomie optimistic-lock (większe szanse na konflikt). Operacja > 30 sekund powinna być dzielona na paczki.
### 13.2 Filtrowanie serwerowe zamiast `.ToList().Where(...)`
`ToList()` materializuje całą tabelę do pamięci. To anti-pattern w kodzie biznesowym.
---
## 14. Czystość API publicznego
### 14.1 Publiczne metody dokumentują kontrakt sesyjny
Metoda przyjmująca `Login` zarządza sesją sama. Metoda przyjmująca `Session` / `ISessionable` deleguje zarządzanie do wywołującego. **Nie mieszaj** — albo metoda otwiera własną sesję, albo dostaje ją z zewnątrz.
### 14.2 Brak `static` metod modyfikujących stan biznesowy
`static void DodajTowar(string kod)` → skąd `Login`? Globalne pole? Pułapka thread-safety. Zawsze przekazuj `Login` / `Session` jawnie.
### 14.3 Nazewnictwo zgodne z konwencją projektu
| Element | Konwencja |
|---------|-----------|
| Parametr `bool` | nazwany (np. `readOnly: true`) |
| Logika biznesowa | nazwy polskie (`Towar`, `Faktura`) |
| Klasy systemowe | nazwy angielskie (`Session`, `Module`) |
---
## 15. Code review checklist (TL;DR)
Do szybkiej weryfikacji PR-a / refaktoringu:
**Sesje i obiekty sesyjne**
- [ ] Każda sesja w `using` (§1.1)
- [ ] Obiekty z innych sesji przepuszczone przez `session.Get(...)` (§2.1)
- [ ] Brak zwracania obiektów sesyjnych poza `using` (§2.2)
- [ ] Brak współdzielenia `Session`/`Row`/`Table`/`Module`/`Context` między wątkami (§3.1)
- [ ] Brak statycznych pól trzymających obiekty sesyjne (§3.2)
**Konflikty i wyjątki**
- [ ] `RowConflictException` z `Save()` ma dedykowaną obsługę (§4.1)
- [ ] Brak ignorowania wyjątku z `Save()` (§4.2)
- [ ] Brak `catch (Exception)` bez konkretnego typu (§9.1)
- [ ] Brak `throw new Exception(...)` — używane typowane (`RowException`, `BusException`, `InvalidOperationException`, …) (§9.2)
**Walidacja**
- [ ] Walidacja rzuca wyjątek **przed** `Commit()` lub w `SavingRow` (§5.1)
- [ ] Komunikaty błędów przez `.Translate()` (§5.2, §11.1)
**Dane**
- [ ] Filtrowanie przez `SubTable[condition]`, nie pętle z `if` w pamięci (§6.1)
- [ ] Brak `View` w kodzie biznesowym (§6.2)
- [ ] Brak pełnego skanu tabel kartotekowych; tabele operacyjne guided z zakresem czasowym; nie-guided w zakresie roota (§6.3)
- [ ] Brak referencji do UI (`IsXxx`, `GetXxx`, `MessageBox`, `IUIServices`) w kodzie biznesowym (§7.1)
- [ ] Brak sprawdzania `AccessRight` w logice biznesowej (§7.2)
- [ ] Z `ExecuteConfig` wracają tylko prymitywy / kopie, nie obiekty sesyjne (§8.1)
- [ ] Brak `Session.ConfigSession` — dostęp do konfiguracji przez `Session.ExecuteConfig()` (§8.2)
**Typy**
- [ ] Pieniądze: `Currency`/`DoubleCy` z walutą (§10.1)
- [ ] Ilości: `Quantity` z jednostką (§10.1)
- [ ] Daty: `Soneta.Types.Date`, `Date.Today` (§10.1, §10.2)
**Tłumaczenia i logowanie**
- [ ] Łańcuchy widoczne dla użytkownika przez `.Translate()` (§11.1)
- [ ] Klucze / symbole / kody bez `Translate` (§11.2)
- [ ] Brak PII / haseł / tokenów w logach (§12.1)
**Wydajność**
- [ ] Krótkie transakcje edycyjne, duże operacje dzielone na paczki (§13.1)
- [ ] Brak `.ToList().Where(...)` w miejsce filtra serwerowego (§13.2)
**API i konwencje**
- [ ] Spójny kontrakt sesyjny — `Login` albo `Session`, nie mieszane (§14.1)
- [ ] Brak `static` metod modyfikujących stan biznesowy (§14.2)
- [ ] Parametry `bool` nazwane (§14.3)
- [ ] Nazewnictwo: logika biznesowa po polsku, klasy systemowe po angielsku (§14.3)