# 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)