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

6.9 KiB
Raw Blame History

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ą (DefinicjaNumeracjiRecordDefinicjaNumeracji);
    • 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:
    dotnet tool install -g dotnet-script
    

Uruchomienie

dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
    -- <NazwaRow> <KatalogDll>

Przykład

dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
    -- DokumentHandlowy ./bin/Debug/net8.0

Przykładowe wyjście

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