110 lines
6.9 KiB
Markdown
110 lines
6.9 KiB
Markdown
# 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 5–8) 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`.
|