Files
soneta-erp-skills/soneta-programming/references/scan-props.md
T
2026-05-19 03:53:43 +02:00

110 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Skanowanie pól klasy biznesowych z DLL (Roslyn MetadataReference)
Narzędzie do odczytu rzeczywistych pól bazodanowych obiektu biznesowego ze skompilowanych bibliotek dodatku
enova365/Soneta.
Pozwala to na budowanie kodu opierającego się na tych klasach biznesowych, wyrażeń bindujących form.xml oraz
(Warunki filtrujące)[./rowconditions.md].
## Cel
W modelu Soneta klasa `Row` (np. `DokumentHandlowy`) udostępnia właściwości publiczne, które można wykorzystać w
generowanych kodzie biznesowym, oraz do budowania wyrażeń bindujących form.xml. Natomiast w
(warunkach filtrujących)[./rowconditions.md], można używać TYLKO pól bazodananowych udostępnianych przez to narzędzie.
Używaj tego narzędzia, gdy:
- piszesz kod operujący bezpośrednio na polach rekordu (np. w extenderze, workerze, datapack);
- chcesz zweryfikować rzeczywisty typ pola (np. `decimal?` vs `decimal`) bez czytania wygenerowanego kodu;
- pracujesz na dodatku innej osoby i nie masz dostępu do źródeł, tylko do DLL.
- generujesz warunki filtrujące (serwerowe - LINQ)
- Przygotowujesz formularze form.xml.
## Mechanizm
Skrypt używa **Roslyn** (`Microsoft.CodeAnalysis.CSharp`) i `MetadataReference.CreateFromFile`, co oznacza, że **metadane są czytane bez ładowania IL** do CLR — bezpiecznie, bez ryzyka konfliktów wersji, x86/x64 itp.
Algorytm:
1. Zbierz wszystkie `*.dll` z podanego katalogu i zarejestruj jako `MetadataReference`. Dodatkowo dołącz wszystkie biblioteki runtime'u .NET z listy TPA (`AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`) — bez tego Roslyn nie rozwiązuje typu `System.ComponentModel.DescriptionAttribute` (i podobnych) i `ConstructorArguments` atrybutów zwraca pustą tablicę, przez co `Tytuł`/`Opis` zostają puste.
2. Zbuduj `CSharpCompilation` z tymi referencjami.
3. Przejdź rekurencyjnie po `IAssemblySymbol.GlobalNamespace` każdej referencji.
4. Znajdź pierwszy typ kończący się na `Module`, który zawiera typ zagnieżdżony o nazwie `{NazwaRekordu}Record`.
5. Odczytaj publiczne pola (`IFieldSymbol`, `DeclaredAccessibility == Public`) i ich typy → oznacz jako **bazodanowe**.
6. Znajdź publiczną klasę najwyższego poziomu o nazwie `{NazwaRekordu}` (klasę biznesową, np. `DokumentHandlowy`) i wczytaj jej publiczne, instancyjne `IPropertySymbol` (wraz z dziedziczonymi).
7. Scal listy:
- property o nazwie unikalnej (brak takiego pola w rekordzie) → oznacz jako **kalkulowane**;
- property o nazwie pokrywającej się z polem rekordu → zachowaj znacznik **bazodanowe**, ale podmień typ na ten z property (bo property zwykle precyzuje typ, np. zwraca konkretny enum lub `Row` zamiast `Guid`/`int`).
8. Dla każdego wpisu odczytaj `Tytuł` i `Opis` — pierwszy parametr `string` konstruktora atrybutu. Matching nazwy atrybutu jest dopasowywany do `Caption`/`CaptionAttribute` oraz `Description`/`DescriptionAttribute` (np. `System.ComponentModel.DescriptionAttribute` używany w generowanym kodzie Soneta). Kolejność źródeł:
1. property klasy biznesowej (`{NazwaRekordu}`) — z uwzględnieniem dziedziczenia (atrybut może być na property bazowej klasy, np. `{NazwaRekordu}Row`);
2. pole rekordu (`{NazwaModulu}+{NazwaRekordu}Record`);
3. odpowiadający member w typie zagnieżdżonym `{NazwaModulu}+{NazwaRekordu}Row` (fallback — przeszukiwany wraz z klasami bazowymi). To tam Soneta zwykle deklaruje `[Description("...")]` na publicznych property delegujących do pola rekordu.
9. **Rekurencja po subrowach** — jeśli któreś z pól rekordu ma typ kończący się na `Record` (np. `CoreModule.DefinicjaNumeracjiRecord Numeracja`), traktuj je jako subrow:
- na bazie nazwy typu wylicz nazwę bazową (`DefinicjaNumeracjiRecord``DefinicjaNumeracji`);
- znajdź klasę biznesową (`DefinicjaNumeracji`) oraz typ `*Module+DefinicjaNumeracjiRow` (mogą być w innym module — np. `CoreModule`);
- powtórz całą procedurę (kroki 58) dla tego rekordu, używając prefiksu `Numeracja.` w kluczach wyników (`Numeracja.Pole1`, `Numeracja.Pole2`, …).
Rekurencja działa dowolnie głęboko (subrow w subrowie). Pętle (rekord zawierający siebie pośrednio) są zabezpieczone przez zbiór odwiedzonych typów.
10. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`).
## Wymagania
- .NET SDK (8.0+)
- `dotnet-script`:
```bash
dotnet tool install -g dotnet-script
```
## Uruchomienie
```bash
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
-- <NazwaRow> <KatalogDll>
```
### Przykład
```bash
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
-- DokumentHandlowy ./bin/Debug/net8.0
```
### Przykładowe wyjście
```markdown
# Pola i właściwości klasy biznesowej: `Soneta.Handel.DokumentHandlowy`
Nazwa tabeli: `DokHandlowe`
- pola bazodanowe: 128
- pola kalkulowane (z klas biznesowych): 388
| Pole | Typ | Rodzaj | Tytuł | Opis |
|------|-----|--------|-------|------|
| Brutto | `decimal` | bazodanowe | Brutto | Wartość brutto dokumentu |
| DataDokumentu | `System.DateTime` | bazodanowe | Data dokumentu | |
| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe | Kontrahent | |
| Netto | `decimal` | bazodanowe | Netto | |
| Numer | `string` | bazodanowe | Numer | |
| SaldoWaluta | `decimal` | | Saldo w walucie | |
| ... | ... | ... | ... | ... |
```
Kolumna `Rodzaj` ma wartość `bazodanowe` dla pól rekordu lub jest pusta dla właściwości kalkulowanych.
## Kody wyjścia
| Kod | Znaczenie |
|-----|-----------|
| `0` | OK — wypisano tabelę pól |
| `1` | Błąd argumentów / nie istnieje katalog / brak DLL |
| `2` | Nie znaleziono typu `*Module+{NazwaRekordu}Record` w referencjach |
## Ograniczenia
- Skanuje tylko górny poziom katalogu (`SearchOption.TopDirectoryOnly`) — jeśli DLL są rozproszone, skopiuj je do jednego katalogu.
- Zwraca pierwszy znaleziony typ pasujący do wzorca `*Module+{Nazwa}Record` — jeśli dwa moduły mają taki sam zagnieżdżony rekord, dostaniesz tylko jeden (niedeterministycznie wg kolejności assembly).
- Zwraca **publiczne pola** rekordu (`IFieldSymbol`) oraz **publiczne, instancyjne właściwości** klasy biznesowej (`IPropertySymbol`, łącznie z dziedziczonymi). Pola rekordu = źródło prawdy o schemacie DB (rodzaj `bazodanowe`); właściwości spoza rekordu = wyliczane w kodzie (rodzaj `kalkulowane`).
- Jeśli klasa biznesowa o nazwie `{NazwaRekordu}` nie zostanie znaleziona w referencjach, skrypt zwraca tylko listę pól bazodanowych (z odpowiednią adnotacją w nagłówku) i kończy się kodem `0`.
- Pierwsze uruchomienie pobiera pakiet NuGet `Microsoft.CodeAnalysis.CSharp` — wymaga połączenia internetowego (kolejne odpalenia działają offline).
## Powiązania
- Patrz [datapack-guidedrow.md](datapack-guidedrow.md) — struktury `GuidedRow` / `ExportedRow` i mechanizm Datapack operujący na polach rekordu.
- Patrz skill `soneta-business-xml` — definicja schematu, z którego `BusinessGenerator` produkuje klasę `XxxRecord`.