Files
soneta-erp-skills/soneta-programming/references/safe-code.md
T
Marcin Wojas 98e3ead84d safe-code.md
2026-05-19 10:57:38 +02:00

15 KiB

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.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.

// Ź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).

// ŹLE
faktura.Kontrahent = kontrahentZInnejSesji;

// DOBRZE
faktura.Kontrahent = session.Get(kontrahentZInnejSesji);

2.2 Nie zwracaj obiektów sesyjnych poza using

// Ź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.

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

throw new RowException(this, "Pole {0} jest wymagane".Translate(), nameof(Nazwa));

6. Filtrowanie po stronie serwera

6.1 Nie iteruj całej tabeli z if w pamięci

// Ź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.

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.

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.

// Ź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.

// 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

// Ź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.

// Ź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.

// Ź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
// Ź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()

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.


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)