scan-modules.csx + scan-props.csx
This commit is contained in:
+35
-13
@@ -17,21 +17,23 @@ Skill zawiera dokumentację fundamentalnych klas logiki biznesowej platformy eno
|
|||||||
|
|
||||||
SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzorce. Po szczegóły konkretnego tematu sięgaj do referencji:
|
SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzorce. Po szczegóły konkretnego tematu sięgaj do referencji:
|
||||||
|
|
||||||
| Temat | Gdzie szukać |
|
| Temat | Gdzie szukać |
|
||||||
|---|---|
|
|---------------------------------------------------------------------------------------------------|---|
|
||||||
| Hierarchia ORM, Row / Table / Module, klucze, ISessionable | sekcje poniżej |
|
| Hierarchia ORM, Row / Table / Module, klucze, ISessionable | sekcje poniżej |
|
||||||
| Sesje, transakcje, Login, Database, BusApplication, optimistic locking | [references/session-login.md](references/session-login.md) |
|
| Sesje, transakcje, Login, Database, BusApplication, optimistic locking | [references/session-login.md](references/session-login.md) |
|
||||||
| Paczki danych, Datapack, GuidedRow, ExportedRow, synchronizacja, blokady | [references/datapack-guidedrow.md](references/datapack-guidedrow.md) |
|
| Paczki danych, Datapack, GuidedRow, ExportedRow, synchronizacja, blokady | [references/datapack-guidedrow.md](references/datapack-guidedrow.md) |
|
||||||
| Klasa Context - dane z UI, zaznaczenia, parametry workera | [references/context.md](references/context.md) |
|
| Klasa Context - dane z UI, zaznaczenia, parametry workera | [references/context.md](references/context.md) |
|
||||||
| Klasy parametrów (ContextBase) - filtry, trwałość, InvokeChanged | [references/contextbase.md](references/contextbase.md) |
|
| Klasy parametrów (ContextBase) - filtry, trwałość, InvokeChanged | [references/contextbase.md](references/contextbase.md) |
|
||||||
| Obiekty Worker i Extender - rozszerzenia modelu, akcje w menu Czynności | [references/worker-extender.md](references/worker-extender.md) |
|
| Obiekty Worker i Extender - rozszerzenia modelu, akcje w menu Czynności | [references/worker-extender.md](references/worker-extender.md) |
|
||||||
| Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) |
|
| Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) |
|
||||||
| Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) |
|
| Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) |
|
||||||
| Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) |
|
| Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) |
|
||||||
| RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) |
|
| RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) |
|
||||||
| ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) |
|
| ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) |
|
||||||
| Cechy (Features) - tabela Features, typy cech, dostęp typowany/nietypowany, bindowanie w form.xml | [references/features.md](references/features.md) |
|
| Cechy (Features) - tabela Features, typy cech, dostęp typowany/nietypowany, bindowanie w form.xml | [references/features.md](references/features.md) |
|
||||||
| Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) |
|
| Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) |
|
||||||
|
| Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.md) |
|
||||||
|
| Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) |
|
||||||
|
|
||||||
## Architektura warstw
|
## Architektura warstw
|
||||||
|
|
||||||
@@ -313,6 +315,26 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: "
|
|||||||
|
|
||||||
Więcej wzorców (kasowanie, obsługa błędów, pełny import end-to-end) - patrz [references/examples.md](references/examples.md).
|
Więcej wzorców (kasowanie, obsługa błędów, pełny import end-to-end) - patrz [references/examples.md](references/examples.md).
|
||||||
|
|
||||||
|
## Narzędzia pomocnicze
|
||||||
|
|
||||||
|
Skill udostępnia skrypt `scripts/scan-props.csx` (uruchamiany przez `dotnet script`) do odczytu publicznych pól klasy zagnieżdżonej `XxxModule+XxxRecord` ze skompilowanych DLL dodatku — bez ładowania IL do CLR (Roslyn `MetadataReference.CreateFromFile`). Wypisuje również właściwości klasy biznesowej (kalkulowane), Caption/Description oraz rekurencyjnie rozwija pola typu subrow z notacją kropkową.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \
|
||||||
|
-- DokumentHandlowy ./bin/Debug/net8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Szczegóły, kody wyjścia i ograniczenia: [references/scan-props.md](references/scan-props.md).
|
||||||
|
|
||||||
|
Drugi skrypt — `scripts/scan-modules.csx` — listuje wszystkie moduły (`*Module` dziedziczące z `Soneta.Business.Module`) oraz znajdujące się w nich tabele (`RowType`/`TableType` z Caption/Description). Pomocne przy wstępnej inwentaryzacji bibliotek, zanim sięgniesz po `scan-props.csx` dla konkretnej tabeli.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-modules.csx \
|
||||||
|
-- ./bin/Debug/net8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Szczegóły: [references/scan-modules.md](references/scan-modules.md).
|
||||||
|
|
||||||
## Konwencje nazewnicze
|
## Konwencje nazewnicze
|
||||||
|
|
||||||
| Element | Konwencja | Przykład |
|
| Element | Konwencja | Przykład |
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Skanowanie modułów i tabel z DLL (Roslyn MetadataReference)
|
||||||
|
|
||||||
|
Narzędzie do wylistowania wszystkich modułów (`*Module`) platformy enova365/Soneta oraz tabel
|
||||||
|
(`*Row` / `*Table`) zdefiniowanych w każdym z nich. Czyta metadane skompilowanych bibliotek dodatku,
|
||||||
|
nie wymaga źródeł.
|
||||||
|
|
||||||
|
## Cel
|
||||||
|
|
||||||
|
W modelu Soneta każda baza danych jest opisana zbiorem modułów (`HandelModule`, `KadryModule`,
|
||||||
|
`CoreModule`, …), a każdy moduł zawiera zagnieżdżone klasy `*Row` definiujące pojedyncze tabele.
|
||||||
|
Skrypt pozwala szybko zinwentaryzować całą strukturę: jakie moduły są obecne w bibliotekach,
|
||||||
|
jakie tabele zawierają i jak nazywa się klasa `*Table` używana w sesji (`Session.Tables.*`).
|
||||||
|
|
||||||
|
Używaj tego narzędzia, gdy:
|
||||||
|
- eksplorujesz nieznany zestaw bibliotek i chcesz zobaczyć pełną listę modułów/tabel;
|
||||||
|
- chcesz znaleźć właściwą nazwę `RowType` lub `TableType` przed użyciem skryptu
|
||||||
|
[scan-props](./scan-props.md);
|
||||||
|
- przygotowujesz dodatek/raport, który potrzebuje pełnego mapowania klasa biznesowa ↔ nazwa tabeli;
|
||||||
|
- weryfikujesz, że nowy dodatek został poprawnie zarejestrowany (jego `*Module` pojawia się na liście).
|
||||||
|
|
||||||
|
## Mechanizm
|
||||||
|
|
||||||
|
Skrypt używa **Roslyn** (`Microsoft.CodeAnalysis.CSharp`) i `MetadataReference.CreateFromFile`,
|
||||||
|
czyli 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 biblioteki runtime'u .NET z listy TPA
|
||||||
|
(`AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`) — bez tego Roslyn nie rozwiązuje
|
||||||
|
`CaptionAttribute` / `DescriptionAttribute` i `ConstructorArguments` 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. Wybierz wszystkie publiczne klasy top-level o nazwie kończącej się na `Module`,
|
||||||
|
które dziedziczą z `Soneta.Business.Module` (sprawdzane po `BaseType`).
|
||||||
|
Filtr eliminuje "śmieci" w stylu `System.Reflection.RuntimeModule`.
|
||||||
|
5. Dla każdego modułu:
|
||||||
|
- znajdź zagnieżdżone klasy o nazwie kończącej się na `Row` (`module.GetTypeMembers()`);
|
||||||
|
- `RowType` = nazwa klasy bez sufiksu `Row` (np. `DokumentHandlowyRow` → `DokumentHandlowy`);
|
||||||
|
- `TableType` = nazwa typu property `Table` w klasie `*Row` (przeszukiwany wraz z dziedziczeniem
|
||||||
|
przez `FindMemberInherited`);
|
||||||
|
- `Tytuł` = `CaptionAttribute`, `Opis` = `DescriptionAttribute` z klasy `*Table` zagnieżdżonej
|
||||||
|
w tym samym module (np. `HandelModule.DokumentHandlowyTable`). Atrybuty są deklarowane
|
||||||
|
w l.mn. („Dokumenty handlowe"), bo opisują tabelę. Fallback: jeśli klasy `*Table` brak
|
||||||
|
lub nie ma atrybutu, czytane są te same atrybuty z klasy `*Row`. Wartością jest pierwszy
|
||||||
|
parametr `string` konstruktora atrybutu.
|
||||||
|
- `Guided` = `tak`, gdy klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable`
|
||||||
|
albo `ExportedTable`. Tabele oznaczone `Guided=tak` są **rootami drzewa obiektów** —
|
||||||
|
stanowią korzeń paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane
|
||||||
|
przez mechanizm synchronizacji i eksportu/importu. Tabele bez tej flagi to elementy
|
||||||
|
szczegółowe (subrowy, info-rowy), które są częścią paczki danej tabeli-korzenia, ale nie
|
||||||
|
stanowią samodzielnego rootu.
|
||||||
|
- Dla samego modułu (`*Module`) Tytuł/Opis czytane są analogicznie z atrybutów na klasie modułu.
|
||||||
|
6. Wypisz markdown: sekcja `##` per moduł (z jego `Caption`/`Description` jeśli są), w każdej
|
||||||
|
sekcji tabela `RowType | TableType | 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-modules.csx \
|
||||||
|
-- <KatalogDll>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Przykład
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-modules.csx \
|
||||||
|
-- ./bin/Debug/net8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Przykładowe wyjście
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Moduły i tabele (Soneta)
|
||||||
|
|
||||||
|
Znaleziono modułów: 37
|
||||||
|
|
||||||
|
## `Soneta.Handel.HandelModule`
|
||||||
|
|
||||||
|
- Opis: Moduł handlowy obsługujący dokumenty sprzedaży, zakupu, zamówień i innych operacji handlowych...
|
||||||
|
- Tabel: 62
|
||||||
|
|
||||||
|
| RowType | TableType | Guided | Tytuł | Opis |
|
||||||
|
|---------|-----------|--------|-------|------|
|
||||||
|
| DefDokHandlowego | DefDokHandlowych | tak | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... |
|
||||||
|
| DefRelacjiHandlowej | DefRelHandlowych | tak | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... |
|
||||||
|
| DokumentHandlowy | DokHandlowe | tak | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... |
|
||||||
|
| DokumentHandlowyKoszt | DokHandloweKoszt | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... |
|
||||||
|
| DrukarkaFiskalna | DrukarkiFiskalne | tak | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... |
|
||||||
|
| ... | ... | ... | ... | ... |
|
||||||
|
|
||||||
|
_Łącznie tabel: 1196_
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kody wyjścia
|
||||||
|
|
||||||
|
| Kod | Znaczenie |
|
||||||
|
|-----|-----------|
|
||||||
|
| `0` | OK — wypisano listę modułów i tabel |
|
||||||
|
| `1` | Błąd argumentów / nie istnieje katalog / brak DLL |
|
||||||
|
|
||||||
|
## Ograniczenia
|
||||||
|
|
||||||
|
- Skanuje tylko górny poziom katalogu (`SearchOption.TopDirectoryOnly`) — jeśli DLL są
|
||||||
|
rozproszone, skopiuj je do jednego katalogu.
|
||||||
|
- `TableType` dla abstrakcyjnych klas `*Row` (subrowy, klasy bazowe) jest często równy `Table` —
|
||||||
|
to znaczy, że property `Table` pochodzi z klasy bazowej `Soneta.Business.Row` i zwraca ogólny
|
||||||
|
typ `Soneta.Business.Table`, a klasa `*Row` nie ma własnej, dedykowanej tabeli.
|
||||||
|
- Pierwsze uruchomienie pobiera pakiet NuGet `Microsoft.CodeAnalysis.CSharp` — wymaga
|
||||||
|
połączenia internetowego (kolejne odpalenia działają offline).
|
||||||
|
|
||||||
|
## Typowy workflow
|
||||||
|
|
||||||
|
1. **Wstępna inwentaryzacja** — uruchom `scan-modules.csx`, żeby zobaczyć pełną listę
|
||||||
|
`RowType`/`TableType`.
|
||||||
|
2. **Drążenie szczegółów** — dla wybranego `RowType` (np. `DokumentHandlowy`) uruchom
|
||||||
|
[scan-props.csx](./scan-props.md) i odczytaj listę pól bazodanowych oraz właściwości
|
||||||
|
kalkulowanych klasy biznesowej.
|
||||||
|
3. **Generowanie kodu / form.xml / warunków** — użyj odczytanych nazw i typów do budowania
|
||||||
|
wyrażeń bindujących, warunków filtrujących, kodu workerów lub Datapacków.
|
||||||
|
|
||||||
|
## Powiązania
|
||||||
|
|
||||||
|
- [scan-props.md](./scan-props.md) — drugi skrypt skanujący, dla pojedynczego rekordu wypisuje
|
||||||
|
pełną listę pól (bazodanowych + kalkulowanych) wraz z `Tytuł`/`Opis` i rekurencyjnym
|
||||||
|
rozwinięciem subrowów.
|
||||||
|
- Patrz skill `soneta-business-xml` — definicje schematu z których `BusinessGenerator`
|
||||||
|
produkuje klasy `*Module`, `*Row`, `*Table` i `*Record`.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.11.0"
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
|
if (Args.Count < 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Użycie: dotnet script scan-modules.csx -- <KatalogDll>");
|
||||||
|
Console.Error.WriteLine("Przykład: dotnet script scan-modules.csx -- ./bin/Debug/net8.0");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllDir = Path.GetFullPath(Args[0]);
|
||||||
|
if (!Directory.Exists(dllDir))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Katalog nie istnieje: {dllDir}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllPaths = Directory.EnumerateFiles(dllDir, "*.dll", SearchOption.TopDirectoryOnly).ToList();
|
||||||
|
if (dllPaths.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Brak plików *.dll w katalogu: {dllDir}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refs = new List<MetadataReference>();
|
||||||
|
var addedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var p in dllPaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
refs.Add(MetadataReference.CreateFromFile(p));
|
||||||
|
addedPaths.Add(Path.GetFileName(p));
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Console.Error.WriteLine($"# Pominięto {Path.GetFileName(p)}: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpa = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? "")
|
||||||
|
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var path in tpa)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(path);
|
||||||
|
if (addedPaths.Contains(name)) continue;
|
||||||
|
try { refs.Add(MetadataReference.CreateFromFile(path)); addedPaths.Add(name); }
|
||||||
|
catch { /* pomiń */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
var compilation = CSharpCompilation.Create("ScanModules")
|
||||||
|
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
|
||||||
|
.AddReferences(refs);
|
||||||
|
|
||||||
|
var modules = new List<INamedTypeSymbol>();
|
||||||
|
foreach (var asmRef in compilation.References)
|
||||||
|
{
|
||||||
|
if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue;
|
||||||
|
foreach (var type in EnumerateAllTypes(asm.GlobalNamespace))
|
||||||
|
{
|
||||||
|
if (type.ContainingType != null) continue;
|
||||||
|
if (type.TypeKind != TypeKind.Class) continue;
|
||||||
|
if (!type.Name.EndsWith("Module")) continue;
|
||||||
|
if (!InheritsFromModule(type)) continue;
|
||||||
|
modules.Add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modules = modules.OrderBy(m => m.ToDisplayString(), StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
|
Console.WriteLine("# Moduły i tabele (Soneta)");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Znaleziono modułów: {modules.Count}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var totalRows = 0;
|
||||||
|
foreach (var module in modules)
|
||||||
|
{
|
||||||
|
// Indeks zagnieżdżonych typów *Table — Row bez własnej klasy *Table nie jest realną tabelą
|
||||||
|
// (jest subrow/abstrakcyjną klasą bazową), więc filtrujemy go z wyników.
|
||||||
|
var tableClasses = module.GetTypeMembers()
|
||||||
|
.Where(t => t.TypeKind == TypeKind.Class && t.Name.EndsWith("Table"))
|
||||||
|
.ToDictionary(t => t.Name, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var rowClasses = module.GetTypeMembers()
|
||||||
|
.Where(t => t.TypeKind == TypeKind.Class && t.Name.EndsWith("Row"))
|
||||||
|
.Where(t => tableClasses.ContainsKey(
|
||||||
|
t.Name.Substring(0, t.Name.Length - "Row".Length) + "Table"))
|
||||||
|
.OrderBy(t => t.Name, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var moduleShortName = module.Name.EndsWith("Module")
|
||||||
|
? module.Name.Substring(0, module.Name.Length - "Module".Length)
|
||||||
|
: module.Name;
|
||||||
|
Console.WriteLine($"## {moduleShortName}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"- Klasa: `{module.ToDisplayString()}`");
|
||||||
|
Console.WriteLine();
|
||||||
|
var modCaption = GetAttributeFirstString(module, "CaptionAttribute");
|
||||||
|
var modDescription = GetAttributeFirstString(module, "DescriptionAttribute");
|
||||||
|
if (!string.IsNullOrEmpty(modCaption)) Console.WriteLine($"- Tytuł: {modCaption}");
|
||||||
|
if (!string.IsNullOrEmpty(modDescription)) Console.WriteLine($"- Opis: {modDescription}");
|
||||||
|
Console.WriteLine($"- Tabel: {rowClasses.Count}");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
if (rowClasses.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("_Brak klas `*Row` w tym module._");
|
||||||
|
Console.WriteLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("| RowType | TableType | Guided | Tytuł | Opis |");
|
||||||
|
Console.WriteLine("|---------|-----------|--------|-------|------|");
|
||||||
|
foreach (var row in rowClasses)
|
||||||
|
{
|
||||||
|
var rowType = row.Name.EndsWith("Row")
|
||||||
|
? row.Name.Substring(0, row.Name.Length - "Row".Length)
|
||||||
|
: row.Name;
|
||||||
|
|
||||||
|
var tableProp = FindMemberInherited(row, "Table") as IPropertySymbol;
|
||||||
|
var tableType = tableProp?.Type is INamedTypeSymbol nt
|
||||||
|
? nt.Name
|
||||||
|
: (tableProp?.Type?.ToDisplayString() ?? "");
|
||||||
|
|
||||||
|
// Atrybuty Caption/Description siedzą na klasie *Table zagnieżdżonej w module
|
||||||
|
// (np. `HandelModule.DokumentHandlowyTable`). Klasa *Row jest fallbackiem na wypadek,
|
||||||
|
// gdyby konwencja w przyszłości się zmieniła.
|
||||||
|
tableClasses.TryGetValue(rowType + "Table", out var tableCls);
|
||||||
|
var caption = GetAttributeFirstString(tableCls, "CaptionAttribute");
|
||||||
|
if (string.IsNullOrEmpty(caption))
|
||||||
|
caption = GetAttributeFirstString(row, "CaptionAttribute");
|
||||||
|
var description = GetAttributeFirstString(tableCls, "DescriptionAttribute");
|
||||||
|
if (string.IsNullOrEmpty(description))
|
||||||
|
description = GetAttributeFirstString(row, "DescriptionAttribute");
|
||||||
|
|
||||||
|
var guided = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls)
|
||||||
|
? "tak"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
Console.WriteLine($"| {rowType} | {tableType} | {guided} | {EscapeCell(caption)} | {EscapeCell(description)} |");
|
||||||
|
totalRows++;
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"_Łącznie tabel: {totalRows}_");
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
static IEnumerable<INamedTypeSymbol> EnumerateAllTypes(INamespaceSymbol ns)
|
||||||
|
{
|
||||||
|
foreach (var t in ns.GetTypeMembers()) yield return t;
|
||||||
|
foreach (var sub in ns.GetNamespaceMembers())
|
||||||
|
foreach (var t in EnumerateAllTypes(sub)) yield return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool InheritsFromGuidedOrExportedTable(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
if (t.Name == "GuidedTable" || t.Name == "ExportedTable") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool InheritsFromModule(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
if (t.Name == "Module" && t.ContainingNamespace?.ToDisplayString() == "Soneta.Business")
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ISymbol FindMemberInherited(INamedTypeSymbol type, string name)
|
||||||
|
{
|
||||||
|
for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
var m = t.GetMembers(name).FirstOrDefault();
|
||||||
|
if (m != null) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName)
|
||||||
|
{
|
||||||
|
if (symbol == null) return "";
|
||||||
|
var shortName = attributeTypeName.EndsWith("Attribute")
|
||||||
|
? attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length)
|
||||||
|
: attributeTypeName;
|
||||||
|
var longName = shortName + "Attribute";
|
||||||
|
foreach (var a in symbol.GetAttributes())
|
||||||
|
{
|
||||||
|
if (a.AttributeClass == null) continue;
|
||||||
|
var n = a.AttributeClass.Name;
|
||||||
|
if (!string.Equals(n, shortName, StringComparison.Ordinal)
|
||||||
|
&& !string.Equals(n, longName, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
foreach (var arg in a.ConstructorArguments)
|
||||||
|
{
|
||||||
|
if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s)
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static string EscapeCell(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "";
|
||||||
|
return s.Replace("\\", "\\\\").Replace("|", "\\|").Replace("\r", " ").Replace("\n", " ");
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.11.0"
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
|
if (Args.Count < 2)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Użycie: dotnet script scan-props.csx -- <NazwaRekordu> <KatalogDll>");
|
||||||
|
Console.Error.WriteLine("Przykład: dotnet script scan-props.csx -- DokumentHandlowy ./bin/Debug/net8.0");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var recordBaseName = Args[0];
|
||||||
|
var dllDir = Path.GetFullPath(Args[1]);
|
||||||
|
var nestedTypeName = recordBaseName + "Record";
|
||||||
|
|
||||||
|
if (!Directory.Exists(dllDir))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Katalog nie istnieje: {dllDir}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllPaths = Directory.EnumerateFiles(dllDir, "*.dll", SearchOption.TopDirectoryOnly).ToList();
|
||||||
|
if (dllPaths.Count == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Brak plików *.dll w katalogu: {dllDir}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refs = new List<MetadataReference>();
|
||||||
|
var addedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var p in dllPaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
refs.Add(MetadataReference.CreateFromFile(p));
|
||||||
|
addedPaths.Add(Path.GetFileName(p));
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Console.Error.WriteLine($"# Pominięto {Path.GetFileName(p)}: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dodaj referencje do bibliotek runtime'u .NET (TPA — Trusted Platform Assemblies),
|
||||||
|
// żeby Roslyn potrafił rozwiązać atrybuty typu System.ComponentModel.DescriptionAttribute
|
||||||
|
// i zdekodować ich argumenty konstruktora.
|
||||||
|
var tpa = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? "")
|
||||||
|
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var path in tpa)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(path);
|
||||||
|
if (addedPaths.Contains(name)) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
refs.Add(MetadataReference.CreateFromFile(path));
|
||||||
|
addedPaths.Add(name);
|
||||||
|
}
|
||||||
|
catch { /* pomiń */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
var compilation = CSharpCompilation.Create("Scan")
|
||||||
|
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
|
||||||
|
.AddReferences(refs);
|
||||||
|
|
||||||
|
INamedTypeSymbol foundRecord = null;
|
||||||
|
INamedTypeSymbol enclosing = null;
|
||||||
|
|
||||||
|
// Indeks publicznych klas najwyższego poziomu po nazwie (do wyszukiwania
|
||||||
|
// klas biznesowych — głównej oraz dla subrowów).
|
||||||
|
var topLevelClasses = new Dictionary<string, INamedTypeSymbol>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var asmRef in compilation.References)
|
||||||
|
{
|
||||||
|
if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue;
|
||||||
|
foreach (var type in EnumerateAllTypes(asm.GlobalNamespace))
|
||||||
|
{
|
||||||
|
if (foundRecord == null && type.Name.EndsWith("Module"))
|
||||||
|
{
|
||||||
|
var nested = type.GetTypeMembers(nestedTypeName).FirstOrDefault();
|
||||||
|
if (nested != null)
|
||||||
|
{
|
||||||
|
foundRecord = nested;
|
||||||
|
enclosing = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type.ContainingType == null
|
||||||
|
&& type.TypeKind == TypeKind.Class
|
||||||
|
&& type.DeclaredAccessibility == Accessibility.Public)
|
||||||
|
{
|
||||||
|
topLevelClasses.TryAdd(type.Name, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundRecord == null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Nie znaleziono typu *Module+{nestedTypeName} w {dllDir}");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
INamedTypeSymbol mainBusinessClass = null;
|
||||||
|
topLevelClasses.TryGetValue(recordBaseName, out mainBusinessClass);
|
||||||
|
|
||||||
|
// Nazwa tabeli wyciągana z typu zwracanego przez property `Table` w klasie XxxxRow.
|
||||||
|
string tableTypeName = null;
|
||||||
|
var rowClass = enclosing?.GetTypeMembers(recordBaseName + "Row").FirstOrDefault();
|
||||||
|
if (rowClass != null)
|
||||||
|
{
|
||||||
|
for (var t = rowClass; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
var tableProp = t.GetMembers("Table").OfType<IPropertySymbol>().FirstOrDefault();
|
||||||
|
if (tableProp != null)
|
||||||
|
{
|
||||||
|
tableTypeName = tableProp.Type.Name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Klucz: nazwa pola z notacją kropkową dla subrowów; Wartość: (typ, czyBazodanowe, tytuł, opis)
|
||||||
|
var merged = new SortedDictionary<string, (string Type, bool IsDb, string Caption, string Description)>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var visited = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
|
||||||
|
ScanRecord(foundRecord, "", visited, merged, topLevelClasses);
|
||||||
|
|
||||||
|
if (mainBusinessClass != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"# Pola i właściwości klasy biznesowej: `{mainBusinessClass.ToDisplayString()}`");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"# Pola i właściwości `{enclosing.ToDisplayString()}+{nestedTypeName}`");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Nie znaleziono klasy biznesowej `{recordBaseName}` — pokazano tylko pola bazodanowe.");
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(tableTypeName))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Nazwa tabeli: `{tableTypeName}`");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
var dbCount = merged.Values.Count(v => v.IsDb);
|
||||||
|
var calcCount = merged.Count - dbCount;
|
||||||
|
Console.WriteLine($"- pola bazodanowe: {dbCount}");
|
||||||
|
Console.WriteLine($"- pola kalkulowane (z klas biznesowych): {calcCount}");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("| Pole | Typ | Rodzaj | Tytuł | Opis |");
|
||||||
|
Console.WriteLine("|------|-----|--------|-------|------|");
|
||||||
|
foreach (var kv in merged)
|
||||||
|
{
|
||||||
|
var rodzaj = kv.Value.IsDb ? "bazodanowe" : "";
|
||||||
|
Console.WriteLine($"| {kv.Key} | `{kv.Value.Type}` | {rodzaj} | {EscapeCell(kv.Value.Caption)} | {EscapeCell(kv.Value.Description)} |");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
static void ScanRecord(
|
||||||
|
INamedTypeSymbol record,
|
||||||
|
string prefix,
|
||||||
|
HashSet<INamedTypeSymbol> visited,
|
||||||
|
SortedDictionary<string, (string Type, bool IsDb, string Caption, string Description)> merged,
|
||||||
|
Dictionary<string, INamedTypeSymbol> topLevelClasses)
|
||||||
|
{
|
||||||
|
if (record == null) return;
|
||||||
|
if (!visited.Add(record)) return;
|
||||||
|
|
||||||
|
var fields = record.GetMembers()
|
||||||
|
.OfType<IFieldSymbol>()
|
||||||
|
.Where(f => f.DeclaredAccessibility == Accessibility.Public)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var encMod = record.ContainingType;
|
||||||
|
var baseName = record.Name.EndsWith("Record")
|
||||||
|
? record.Name.Substring(0, record.Name.Length - "Record".Length)
|
||||||
|
: record.Name;
|
||||||
|
|
||||||
|
INamedTypeSymbol bizCls = null;
|
||||||
|
topLevelClasses.TryGetValue(baseName, out bizCls);
|
||||||
|
var rowFallback = encMod?.GetTypeMembers(baseName + "Row").FirstOrDefault();
|
||||||
|
|
||||||
|
// 1. Pola rekordu → bazodanowe.
|
||||||
|
foreach (var f in fields)
|
||||||
|
{
|
||||||
|
var key = prefix + f.Name;
|
||||||
|
merged[key] = (
|
||||||
|
f.Type.ToDisplayString(),
|
||||||
|
true,
|
||||||
|
GetAttributeFirstString(f, "CaptionAttribute"),
|
||||||
|
GetAttributeFirstString(f, "DescriptionAttribute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Właściwości klasy biznesowej (z dziedziczeniem) → kalkulowane lub nadpisanie pola bazodanowego.
|
||||||
|
if (bizCls != null)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (var p in EnumerateInheritedProperties(bizCls))
|
||||||
|
{
|
||||||
|
if (p.DeclaredAccessibility != Accessibility.Public || p.IsStatic || p.IsIndexer || p.GetMethod == null)
|
||||||
|
continue;
|
||||||
|
if (!seen.Add(p.Name)) continue;
|
||||||
|
var key = prefix + p.Name;
|
||||||
|
var typeStr = p.Type.ToDisplayString();
|
||||||
|
var caption = GetAttributeFirstString(p, "CaptionAttribute");
|
||||||
|
var description = GetAttributeFirstString(p, "DescriptionAttribute");
|
||||||
|
if (merged.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
merged[key] = (
|
||||||
|
typeStr,
|
||||||
|
existing.IsDb,
|
||||||
|
!string.IsNullOrEmpty(caption) ? caption : existing.Caption,
|
||||||
|
!string.IsNullOrEmpty(description) ? description : existing.Description);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
merged[key] = (typeStr, false, caption, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback — atrybuty Caption/Description z typu zagnieżdżonego *Row (wraz z dziedziczeniem).
|
||||||
|
if (rowFallback != null)
|
||||||
|
{
|
||||||
|
var prefixLen = prefix.Length;
|
||||||
|
foreach (var key in merged.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (!key.StartsWith(prefix, StringComparison.Ordinal)) continue;
|
||||||
|
var local = key.Substring(prefixLen);
|
||||||
|
if (local.Length == 0 || local.Contains('.')) continue;
|
||||||
|
var entry = merged[key];
|
||||||
|
if (!string.IsNullOrEmpty(entry.Caption) && !string.IsNullOrEmpty(entry.Description))
|
||||||
|
continue;
|
||||||
|
var member = FindMemberInherited(rowFallback, local);
|
||||||
|
if (member == null) continue;
|
||||||
|
var caption = !string.IsNullOrEmpty(entry.Caption)
|
||||||
|
? entry.Caption
|
||||||
|
: GetAttributeFirstString(member, "CaptionAttribute");
|
||||||
|
var description = !string.IsNullOrEmpty(entry.Description)
|
||||||
|
? entry.Description
|
||||||
|
: GetAttributeFirstString(member, "DescriptionAttribute");
|
||||||
|
merged[key] = (entry.Type, entry.IsDb, caption, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Rekurencja po polach typu subrow (typ kończący się na "Record").
|
||||||
|
foreach (var f in fields)
|
||||||
|
{
|
||||||
|
if (f.Type is INamedTypeSymbol nested && nested.TypeKind == TypeKind.Class && nested.Name.EndsWith("Record"))
|
||||||
|
{
|
||||||
|
ScanRecord(nested, prefix + f.Name + ".", visited, merged, topLevelClasses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<INamedTypeSymbol> EnumerateAllTypes(INamespaceSymbol ns)
|
||||||
|
{
|
||||||
|
foreach (var t in ns.GetTypeMembers()) yield return t;
|
||||||
|
foreach (var sub in ns.GetNamespaceMembers())
|
||||||
|
foreach (var t in EnumerateAllTypes(sub)) yield return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<IPropertySymbol> EnumerateInheritedProperties(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
foreach (var p in t.GetMembers().OfType<IPropertySymbol>())
|
||||||
|
yield return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName)
|
||||||
|
{
|
||||||
|
if (symbol == null) return "";
|
||||||
|
var shortName = attributeTypeName.EndsWith("Attribute")
|
||||||
|
? attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length)
|
||||||
|
: attributeTypeName;
|
||||||
|
var longName = shortName + "Attribute";
|
||||||
|
foreach (var a in symbol.GetAttributes())
|
||||||
|
{
|
||||||
|
if (a.AttributeClass == null) continue;
|
||||||
|
var n = a.AttributeClass.Name;
|
||||||
|
if (!string.Equals(n, shortName, StringComparison.Ordinal)
|
||||||
|
&& !string.Equals(n, longName, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
foreach (var arg in a.ConstructorArguments)
|
||||||
|
{
|
||||||
|
if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s)
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static ISymbol FindMemberInherited(INamedTypeSymbol type, string name)
|
||||||
|
{
|
||||||
|
for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
var m = t.GetMembers(name).FirstOrDefault();
|
||||||
|
if (m != null) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string EscapeCell(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return "";
|
||||||
|
return s.Replace("\\", "\\\\").Replace("|", "\\|").Replace("\r", " ").Replace("\n", " ");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user