scan-workers.csx
This commit is contained in:
@@ -43,6 +43,7 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo
|
|||||||
| **Zasady bezpiecznego kodu biznesowego — checklist do review i refaktoringu** | [references/safe-code.md](references/safe-code.md) |
|
| **Zasady bezpiecznego kodu biznesowego — checklist do review i refaktoringu** | [references/safe-code.md](references/safe-code.md) |
|
||||||
| Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.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) |
|
| Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) |
|
||||||
|
| Inwentaryzacja workerów i extenderów (`[Worker<…>]`) z DLL | [references/scan-workers.md](references/scan-workers.md) |
|
||||||
|
|
||||||
## Architektura warstw
|
## Architektura warstw
|
||||||
|
|
||||||
@@ -326,10 +327,11 @@ Więcej wzorców (kasowanie, obsługa błędów, pełny import end-to-end) - pat
|
|||||||
|
|
||||||
## Narzędzia pomocnicze
|
## Narzędzia pomocnicze
|
||||||
|
|
||||||
Skill udostępnia dwa skrypty `dotnet script` (`scripts/`) do statycznej inwentaryzacji bibliotek Soneta — bez ładowania IL do CLR (Roslyn `MetadataReference.CreateFromFile`):
|
Skill udostępnia trzy skrypty `dotnet script` (`scripts/`) do statycznej inwentaryzacji bibliotek Soneta — bez ładowania IL do CLR (Roslyn `MetadataReference.CreateFromFile`):
|
||||||
|
|
||||||
- `scan-modules.csx` — listuje moduły (`*Module`) i ich tabele (`*Row`/`*Table`) z Caption/Description. Dobre na start. Szczegóły, parametry i przykłady uruchomienia: [references/scan-modules.md](references/scan-modules.md).
|
- `scan-modules.csx` — listuje moduły (`*Module`) i ich tabele (`*Row`/`*Table`) z Caption/Description. Dobre na start. Szczegóły, parametry i przykłady uruchomienia: [references/scan-modules.md](references/scan-modules.md).
|
||||||
- `scan-props.csx` — wypisuje pola i właściwości kalkulowane konkretnej klasy biznesowej, rekurencyjnie po polach typu subrow. Sięgnij po niego, gdy znasz już tabelę i potrzebujesz jej kontraktu. Szczegóły: [references/scan-props.md](references/scan-props.md).
|
- `scan-props.csx` — wypisuje pola i właściwości kalkulowane konkretnej klasy biznesowej, rekurencyjnie po polach typu subrow. Sięgnij po niego, gdy znasz już tabelę i potrzebujesz jej kontraktu. Szczegóły: [references/scan-props.md](references/scan-props.md).
|
||||||
|
- `scan-workers.csx` — wypisuje na stdout **JSON** z workerami i extenderami zarejestrowanymi atrybutem assembly `[Worker<…>]`, pogrupowanymi wg `DataType`. Dla każdej klasy: parametry inicjowane z `Context` (ctor + `[Context]`, z rozwinięciem pod-property dla typów dziedziczących z `ContextBase`), property do bindowania, akcje menu Czynności. Opcjonalny drugi argument filtruje wynik do konkretnego typu danych (np. `DokumentHandlowy`) — w praktyce konieczny, bo pełne skanowanie zwraca tysiące rejestracji. Wynik łatwo przetwarzać `jq`. Szczegóły: [references/scan-workers.md](references/scan-workers.md).
|
||||||
|
|
||||||
## Konwencje nazewnicze
|
## Konwencje nazewnicze
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# Skanowanie workerów i extenderów z DLL (Roslyn MetadataReference)
|
||||||
|
|
||||||
|
Narzędzie do wylistowania wszystkich klas `*Worker` / `*Extender` zarejestrowanych w bibliotekach
|
||||||
|
dodatków enova365 / Soneta przez atrybut `WorkerAttribute` (assembly). Czyta metadane skompilowanych
|
||||||
|
bibliotek dodatku — nie wymaga źródeł.
|
||||||
|
|
||||||
|
## Cel
|
||||||
|
|
||||||
|
W modelu Soneta workery i extendery są rejestrowane atrybutem assembly:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[assembly: Worker<NazwaWorker, TypDanych>] // worker przypięty do typu danych
|
||||||
|
[assembly: Worker<NazwaExtender>] // extender (bez typu danych)
|
||||||
|
```
|
||||||
|
|
||||||
|
Skrypt wyciąga wszystkie takie rejestracje, grupuje workery wg typu danych oraz wypisuje dla każdej
|
||||||
|
klasy: parametry inicjowane z `Context`, property dostępne do bindowania/odczytu oraz pozycje
|
||||||
|
menu Czynności (metody z atrybutem `[Action]`).
|
||||||
|
|
||||||
|
Używaj tego narzędzia, gdy:
|
||||||
|
- robisz inwentaryzację rozszerzeń (workery / extendery) w dodatku innej osoby albo w całej aplikacji;
|
||||||
|
- chcesz znaleźć dostępne workery dla danego typu danych zanim napiszesz form.xml (`{Workers.<Alias>.<Property>}`);
|
||||||
|
- sprawdzasz, jakie pozycje menu Czynności są dostępne na danym obiekcie;
|
||||||
|
- weryfikujesz, że Twój nowy worker / extender został poprawnie zarejestrowany i jest widoczny dla platformy;
|
||||||
|
- przygotowujesz raport dla code review (dodatek powinien mieć spójną listę workerów / akcji).
|
||||||
|
|
||||||
|
Po komplementarne dane — patrz `scan-modules.csx` (lista tabel) i `scan-props.csx` (pola tabeli).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Algorytm:
|
||||||
|
1. Zbierz wszystkie `*.dll` z podanego katalogu, dodaj jako `MetadataReference`. Dołącz biblioteki
|
||||||
|
runtime'u .NET (TPA — `AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`) — bez tego Roslyn
|
||||||
|
nie rozwiązuje atrybutów ramowych i zwraca pustą `ConstructorArguments`.
|
||||||
|
2. Zbuduj `CSharpCompilation` z tymi referencjami.
|
||||||
|
3. Dla każdego `IAssemblySymbol` przejdź po atrybutach assembly (`asm.GetAttributes()`)
|
||||||
|
i odfiltruj te, których klasa to `WorkerAttribute` z namespace zaczynającego się od `Soneta`
|
||||||
|
(chroni przed kolizją z atrybutami o tej samej nazwie z innych bibliotek).
|
||||||
|
4. Wyciągnij dane rejestracji w zależności od wariantu atrybutu:
|
||||||
|
- **wariant generyczny** (`[Worker<TWorker>]`, `[Worker<TWorker, TData>]`) — typy biorę z
|
||||||
|
`AttributeClass.TypeArguments[0]` (worker) oraz `TypeArguments[1]` (data, opcjonalnie).
|
||||||
|
W metadanych klasa atrybutu ma backtick (`WorkerAttribute\`1`, `WorkerAttribute\`2`), ale
|
||||||
|
`INamedTypeSymbol.Name` zwraca `"WorkerAttribute"` bez backticka — wystarczy porównanie po nazwie.
|
||||||
|
- **wariant z parametrami** (`[Worker(typeof(TWorker), typeof(TData))]`) — typy biorę
|
||||||
|
z `ConstructorArguments` (`TypedConstantKind.Type`). Dodatkowy string z konstruktora lub
|
||||||
|
`NamedArgument` `Name` traktuję jako alias bindingu (nadpisuje nazwę domyślną).
|
||||||
|
5. Pogrupuj rejestracje wg `DataType` (workery) i posortuj alfabetycznie. Rejestracje bez
|
||||||
|
`DataType` trafiają pod osobny klucz `__extenders__` w JSON-ie.
|
||||||
|
6. Dla każdej klasy workera / extendera odczytaj:
|
||||||
|
- **Alias bindingu** (pole `name`) — jawnie podany `Name` z atrybutu albo nazwa klasy
|
||||||
|
bez sufiksu `Worker` / pełna nazwa dla extendera. Binding na UI:
|
||||||
|
- worker (lista): `{Workers.<name>.<Property>}`
|
||||||
|
- extender (formularz): `{new <name>.<Property>}`
|
||||||
|
- **Konstruktor inicjowany z Context** — parametry pierwszego publicznego konstruktora
|
||||||
|
z parametrami trafiają do `params` z `kind: "ctor"`.
|
||||||
|
- **Property z `[Context]`** — publiczne instancyjne property z atrybutem `ContextAttribute`,
|
||||||
|
inicjowane z Context — trafiają do `params` bez pola `kind`.
|
||||||
|
- **Pod-parametry typu `ContextBase`** — gdy typ parametru dziedziczy z
|
||||||
|
`Soneta.Business.ContextBase`, wpis dostaje zagnieżdżone `props` z publicznymi property
|
||||||
|
tej klasy (pomija property samego `ContextBase`).
|
||||||
|
- **Property do bindowania / odczytu** (`props` workera) — pozostałe publiczne instancyjne
|
||||||
|
property z getterem (bez `[Context]`).
|
||||||
|
- **Akcje menu Czynności** (`actions`) — publiczne instancyjne metody z atrybutem
|
||||||
|
`ActionAttribute`; każda akcja ma `name` (tytuł), `method` (nazwa metody), `result`
|
||||||
|
(typ wyniku, `void` dla `void`).
|
||||||
|
7. Wypisz JSON na stdout (sformatowany, z polskimi znakami w czystej formie). Sekcje
|
||||||
|
`params` / `actions` / `props` są pomijane, gdy są puste.
|
||||||
|
|
||||||
|
## Wymagania
|
||||||
|
|
||||||
|
- .NET SDK (8.0+)
|
||||||
|
- `dotnet-script`:
|
||||||
|
```bash
|
||||||
|
dotnet tool install -g dotnet-script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uruchomienie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-workers.csx \
|
||||||
|
-- <KatalogDll> [<NazwaTypuDanych>] [--related]
|
||||||
|
```
|
||||||
|
|
||||||
|
Drugi argument (opcjonalny) ogranicza wynik do workerów przypiętych do wskazanego typu danych —
|
||||||
|
pełne skanowanie bibliotek Soneta zwraca tysiące rejestracji, więc filtr jest praktycznie
|
||||||
|
niezbędny w codziennej pracy. Dopasowanie po:
|
||||||
|
- **prostej nazwie** klasy (np. `DokumentHandlowy`), albo
|
||||||
|
- **pełnej nazwie** z namespace (np. `Soneta.Handel.DokumentHandlowy`).
|
||||||
|
|
||||||
|
Gdy filtr jest podany, extendery (rejestracje bez `DataType`) są pomijane — ich nie da się
|
||||||
|
przypisać do typu danych.
|
||||||
|
|
||||||
|
### Flaga `--related` — typy powiązane
|
||||||
|
|
||||||
|
`--related` rozszerza filtr o typy powiązane z podanym typem. Pozwala jednym wywołaniem zebrać
|
||||||
|
workery z całej „rodziny" obiektu (rekord, tabela, historia), bez konieczności trzech osobnych
|
||||||
|
uruchomień. Reguły rozpoznawania powiązań (po metadanych typu):
|
||||||
|
|
||||||
|
| Typ wejściowy | Powiązany typ | Sposób odczytu |
|
||||||
|
|---|---|---|
|
||||||
|
| Klasa dziedzicząca z `Soneta.Business.Row` (np. `Pracownik`, `DokumentHandlowy`) | Klasa tabeli (`Pracownicy`, `DokHandlowe`) | property `Table` w klasie `Row` (lub klas bazowych) |
|
||||||
|
| Klasa dziedzicząca z `Soneta.Business.Table` (np. `Pracownicy`, `DokHandlowe`) | Klasa rekordu (`Pracownik`, `DokumentHandlowy`) | indekser `this[int]` — typ zwracany |
|
||||||
|
| Typ implementujący `IRowWithHistory` (np. `Pracownik`) | Typ rekordu historycznego (`PracHistoria`) | indekser `this[Soneta.Types.Date]` — typ zwracany |
|
||||||
|
|
||||||
|
Reguły działają łącznie — np. dla `Pracownik` (Row + IRowWithHistory) zebrane są workery z trzech
|
||||||
|
typów naraz: `Pracownik`, `Pracownicy`, `PracHistoria`. Informacje o znalezionych typach
|
||||||
|
powiązanych skrypt wypisuje na stderr (`# Typ podstawowy: …`, `# Typ powiązany: …`), żeby nie
|
||||||
|
zaśmiecać JSON-a na stdout.
|
||||||
|
|
||||||
|
Gdy `--related` jest podany, ale typu z `<NazwaTypuDanych>` nie da się znaleźć w referencjach
|
||||||
|
(np. literówka), skrypt loguje ostrzeżenie na stderr i wraca do prostego dopasowania po nazwie.
|
||||||
|
|
||||||
|
### Przykłady
|
||||||
|
|
||||||
|
Pełna inwentaryzacja:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-workers.csx \
|
||||||
|
-- ./bin/Debug/net8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Tylko workery przypięte do `DokumentHandlowy`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-workers.csx \
|
||||||
|
-- ./bin/Debug/net8.0 DokumentHandlowy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format wyjścia: JSON
|
||||||
|
|
||||||
|
Skrypt **zawsze** wypisuje JSON na stdout — nadaje się do dalszego przetwarzania
|
||||||
|
(`jq`, skrypty, narzędzia). Markdown został usunięty, żeby utrzymać jedno, stabilne
|
||||||
|
źródło danych dla automatów i klientów.
|
||||||
|
|
||||||
|
Struktura JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "Workery przypięte do typu `DokumentHandlowy` (Soneta)",
|
||||||
|
"Soneta.Handel.DokumentHandlowy": [
|
||||||
|
{
|
||||||
|
"workerAssembly": "Soneta.Zadania",
|
||||||
|
"workerType": "Soneta.Zadania.Smsing.WyslijSmsWorker",
|
||||||
|
"name": "WyslijSms",
|
||||||
|
"params": [
|
||||||
|
{ "name": "ConstructorParam", "type": "Soneta.X.Y", "kind": "ctor" },
|
||||||
|
{ "name": "PropWithContextAttr", "type": "Soneta.X.Y" },
|
||||||
|
{
|
||||||
|
"name": "Pars",
|
||||||
|
"type": "Soneta.X.SomeWorker.Params",
|
||||||
|
"props": [
|
||||||
|
{ "name": "DataOd", "type": "Soneta.Types.Date" },
|
||||||
|
{ "name": "DataDo", "type": "Soneta.Types.Date" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{ "name": "Wyślij SMS", "method": "WyslijSmsa", "result": "object" }
|
||||||
|
],
|
||||||
|
"props": [
|
||||||
|
{ "name": "PublicPropWithoutContextAttr", "type": "Soneta.X.Y" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Klucze top-level: `description` + jeden klucz na każdy `DataType` (pełna nazwa z namespace).
|
||||||
|
- `params` łączy parametry konstruktora (z `kind: "ctor"`) oraz property z atrybutem `[Context]`
|
||||||
|
(bez pola `kind`) — wszystko, co Soneta inicjuje z `Context` przy tworzeniu workera.
|
||||||
|
- Gdy typ parametru **dziedziczy z `Soneta.Business.ContextBase`** (klasa parametrów workera —
|
||||||
|
zwykle nested `Params` w klasie workera), wpis zawiera dodatkowo `props` z listą publicznych,
|
||||||
|
instancyjnych property tej klasy. To pod-parametry, które użytkownik widzi w oknie parametrów
|
||||||
|
workera. Property samego `ContextBase` (np. `Context`) są pomijane.
|
||||||
|
- `actions` — metody z atrybutem `[Action]`. `name` to tytuł z atrybutu, `method` to nazwa
|
||||||
|
metody w C#, `result` to deklarowany typ wyniku (`void` dla metod bez wartości).
|
||||||
|
- `props` — publiczne, instancyjne property z getterem, bez `[Context]` — kandydaci do
|
||||||
|
bindowania w `form.xml` przez `{Workers.<name>.<Property>}`.
|
||||||
|
- Sekcje puste (`params`/`actions`/`props`) są pomijane, żeby JSON pozostał zwięzły.
|
||||||
|
- Extendery (rejestracje bez `DataType`) trafiają — wyłącznie w trybie bez filtra typu —
|
||||||
|
pod klucz `__extenders__`.
|
||||||
|
|
||||||
|
### Przykłady filtrowania `jq`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lista workerów dla typu:
|
||||||
|
jq '."Soneta.Handel.DokumentHandlowy"[] | .workerType' /tmp/out.json
|
||||||
|
|
||||||
|
# Tylko z akcjami menu Czynności:
|
||||||
|
jq '."Soneta.Handel.DokumentHandlowy"[] | select(.actions)' /tmp/out.json
|
||||||
|
|
||||||
|
# Konkretny worker po aliasie:
|
||||||
|
jq '."Soneta.Handel.DokumentHandlowy"[] | select(.name=="KSeFWyslij")' /tmp/out.json
|
||||||
|
|
||||||
|
# Workery, których parametr `Params` ma pole `Magazyn`:
|
||||||
|
jq '.[] | arrays | .[] | select((.params // [])
|
||||||
|
| map(.props // []) | flatten | map(.name) | index("Magazyn"))' /tmp/out.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kody wyjścia
|
||||||
|
|
||||||
|
| Kod | Znaczenie |
|
||||||
|
|-----|-----------|
|
||||||
|
| `0` | OK — wypisano listę workerów i extenderów |
|
||||||
|
| `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.
|
||||||
|
- Filtruje atrybut `WorkerAttribute` po nazwie i namespace `Soneta*`. Jeśli inny dodatek
|
||||||
|
zarejestruje atrybut o tej samej nazwie w innym namespace, nie zostanie ujęty.
|
||||||
|
- Skrypt wypisuje **publiczne instancyjne** property/metody. Property prywatne lub statyczne
|
||||||
|
są pomijane (nie biorą udziału w bindowaniu / akcjach).
|
||||||
|
- Property z modyfikatorem `internal` nie są ujęte — Soneta wymaga publicznych członków
|
||||||
|
do bindowania UI.
|
||||||
|
- Pierwsze uruchomienie pobiera pakiet NuGet `Microsoft.CodeAnalysis.CSharp` — wymaga
|
||||||
|
połączenia internetowego (kolejne odpalenia działają offline).
|
||||||
|
|
||||||
|
## Typowy workflow
|
||||||
|
|
||||||
|
1. **Inwentaryzacja workerów** — uruchom `scan-workers.csx`, znajdź wszystkie workery
|
||||||
|
zarejestrowane dla interesującego Cię typu danych (np. `DokumentHandlowy`).
|
||||||
|
2. **Wybór aliasu do bindingu** — z sekcji workera odczytaj `Alias` i `Property do bindowania`
|
||||||
|
— to bezpośrednio wartości do podstawienia w `form.xml`:
|
||||||
|
`{Workers.<Alias>.<Property>}` (worker) lub `{new <Alias>.<Property>}` (extender).
|
||||||
|
3. **Lista akcji** — kolumna „Menu Czynności" pokazuje, które pozycje pojawią się
|
||||||
|
w menu Czynności dla danego obiektu.
|
||||||
|
4. **Code review** — porównaj listę z oczekiwaną zawartością dodatku (wszystkie spodziewane
|
||||||
|
rejestracje są obecne, aliasy się nie pokrywają, akcje mają komplet metod sterujących).
|
||||||
|
|
||||||
|
## Powiązania
|
||||||
|
|
||||||
|
- [worker-extender.md](./worker-extender.md) — semantyka workerów/extenderów, atrybut `[Context]`,
|
||||||
|
`[Action]`, metody sterujące `IsVisibleXxx` / `IsEnabledXxx` / `GetNameXxx` / `IsCheckedXxx`,
|
||||||
|
bindowanie w form.xml.
|
||||||
|
- [context.md](./context.md) — jak działa `Context` i co może być źródłem parametrów workera.
|
||||||
|
- [scan-modules.md](./scan-modules.md) — lista modułów i tabel platformy (komplementarne do scan-workers).
|
||||||
|
- [scan-props.md](./scan-props.md) — pola konkretnego rekordu (do których workery doklejają property kalkulowane).
|
||||||
@@ -7,11 +7,13 @@ Oba korzystają z [Context](context.md) do pobierania parametrów.
|
|||||||
## Obiekty Worker
|
## Obiekty Worker
|
||||||
|
|
||||||
Worker dorzuca do obiektu danych dodatkowe properties wyliczane (do użycia w bindowaniu) oraz pozycje w menu Czynności.
|
Worker dorzuca do obiektu danych dodatkowe properties wyliczane (do użycia w bindowaniu) oraz pozycje w menu Czynności.
|
||||||
|
Worker można też **utworzyć i wywołać ręcznie z kodu** — wystarczy zainstancjonować klasę, ustawić jej pola/properties
|
||||||
|
i wywoływać metody (patrz [Programowe użycie workera](#programowe-użycie-workera)).
|
||||||
|
|
||||||
* Przypisuj worker do konkretnego obiektu danych — worker zawsze działa w kontekście jednego typu.
|
* Przypisuj worker do konkretnego obiektu danych — worker zawsze działa w kontekście jednego typu.
|
||||||
* Dodawaj do nazwy klasy sufiks `Worker` (np. `WyliczenieStanMagazynuWorker`).
|
* Dodawaj do nazwy klasy sufiks `Worker` (np. `WyliczenieStanMagazynuWorker`).
|
||||||
* Wybieraj nazwę klasy opisującą działanie, nie technikę.
|
* Wybieraj nazwę klasy opisującą działanie, nie technikę.
|
||||||
* Inicjuj parametry z kontekstu przez `[Context]`.
|
* Inicjuj parametry z kontekstu przez `[Context]` lub przez konstruktor (jego parametry również pobierane są z `Context`).
|
||||||
* Rejestruj przez generyczny atrybut `[assembly: Worker<WorkerType, DataType>]` — to wersja zalecana.
|
* Rejestruj przez generyczny atrybut `[assembly: Worker<WorkerType, DataType>]` — to wersja zalecana.
|
||||||
|
|
||||||
### Rejestracja worker
|
### Rejestracja worker
|
||||||
@@ -26,6 +28,20 @@ Worker dorzuca do obiektu danych dodatkowe properties wyliczane (do użycia w bi
|
|||||||
[assembly: Worker(typeof(NazwaKlasyWorker), typeof(DataType))]
|
[assembly: Worker(typeof(NazwaKlasyWorker), typeof(DataType))]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Opcjonalny alias `name`
|
||||||
|
|
||||||
|
Atrybut `Worker` przyjmuje dodatkowy, opcjonalny parametr `name` — alternatywną nazwę używaną
|
||||||
|
przy bindowaniu w `form.xml` (`{Workers.<name>.<Property>}`). Standardowo aliasem jest nazwa klasy
|
||||||
|
workera **bez sufiksu `Worker`** (`WyliczenieStanMagazynuWorker` → `WyliczenieStanMagazynu`).
|
||||||
|
Parametr `name` ma sens tylko wtedy, gdy chcesz zbindować worker pod inną nazwą niż domyślna —
|
||||||
|
np. dla zachowania kompatybilności po refaktoringu klasy.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[assembly: Worker<NowyWyliczStanuWorker, Towar>("WyliczenieStanMagazynu")]
|
||||||
|
// W form.xml dalej używamy starego aliasu:
|
||||||
|
// EditValue="{Workers.WyliczenieStanMagazynu.StanMagazynu}"
|
||||||
|
```
|
||||||
|
|
||||||
### Deklaracja klasy worker
|
### Deklaracja klasy worker
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -276,6 +292,72 @@ public class StanTowaruWorker
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Konstruktor inicjowany z Context
|
||||||
|
|
||||||
|
Jeśli klasa workera (lub extendera) ma **konstruktor publiczny z parametrami**, jego parametry są
|
||||||
|
inicjowane z `Context` po typie — analogicznie jak property z atrybutem `[Context]`. Pozwala to
|
||||||
|
trzymać pola jako `readonly` i wymusza komplet zależności w momencie tworzenia obiektu.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[assembly: Worker<WyliczenieStanMagazynuWorker, Towar>]
|
||||||
|
|
||||||
|
public class WyliczenieStanMagazynuWorker
|
||||||
|
{
|
||||||
|
private readonly Towar towar;
|
||||||
|
private readonly Magazyn magazyn;
|
||||||
|
|
||||||
|
// Parametry konstruktora są pobierane z Context (po typie) w momencie tworzenia workera.
|
||||||
|
public WyliczenieStanMagazynuWorker(Towar towar, Magazyn magazyn)
|
||||||
|
{
|
||||||
|
this.towar = towar;
|
||||||
|
this.magazyn = magazyn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal StanMagazynu =>
|
||||||
|
magazyn != null ? towar.GetStan(magazyn) : towar.GetStanCalkowity();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reguły:
|
||||||
|
* Jeśli jest więcej niż jeden konstruktor publiczny, platforma wybiera ten, dla którego potrafi
|
||||||
|
rozwiązać komplet parametrów z `Context`.
|
||||||
|
* Konstruktor i property z `[Context]` można łączyć w jednej klasie.
|
||||||
|
* Brak wymaganej zależności w `Context` skutkuje błędem / oknem parametrów (analogicznie jak
|
||||||
|
brakujące `[Context]`).
|
||||||
|
|
||||||
|
## Programowe użycie workera
|
||||||
|
|
||||||
|
Workera można utworzyć i wywołać bez pośrednictwa UI — ręcznie z kodu biznesowego. Wystarczy
|
||||||
|
zainstancjonować klasę, ustawić pola/properties (lub przekazać je przez konstruktor) i wywołać
|
||||||
|
metody.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using (var session = login.CreateSession(readOnly: true, config: false, name: "PoliczStan"))
|
||||||
|
{
|
||||||
|
var towar = session.GetTowary().Towary.WgKodu["NOWY001"];
|
||||||
|
var magazyn = session.GetMagazyny().Magazyny.WgKodu["MAG-A"];
|
||||||
|
|
||||||
|
var worker = new WyliczenieStanMagazynuWorker
|
||||||
|
{
|
||||||
|
Towar = towar,
|
||||||
|
Magazyn = magazyn,
|
||||||
|
};
|
||||||
|
|
||||||
|
decimal stan = worker.StanMagazynu;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Kiedy worker wymaga konstruktora — przekaż zależności jako parametry konstruktora zamiast property:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var worker = new WyliczenieStanMagazynuWorker(towar, magazyn);
|
||||||
|
decimal stan = worker.StanMagazynu;
|
||||||
|
```
|
||||||
|
|
||||||
|
Taki sposób użycia jest przydatny w testach jednostkowych, w workerach wywoływanych z innych
|
||||||
|
workerów oraz w kodzie biznesowym, który chce skorzystać z logiki zamkniętej w workerze bez
|
||||||
|
przechodzenia przez UI.
|
||||||
|
|
||||||
## Dobre praktyki
|
## Dobre praktyki
|
||||||
|
|
||||||
1. **Używaj [Context]** w obiektach worker i extender dla parametrów inicjowanych z context
|
1. **Używaj [Context]** w obiektach worker i extender dla parametrów inicjowanych z context
|
||||||
|
|||||||
@@ -0,0 +1,513 @@
|
|||||||
|
#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.11.0"
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Unicode;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
|
||||||
|
// Argumenty pozycyjne + flagi (np. --related).
|
||||||
|
var positional = Args.Where(a => !a.StartsWith("--", StringComparison.Ordinal)).ToList();
|
||||||
|
var includeRelated = Args.Any(a => a == "--related");
|
||||||
|
|
||||||
|
if (positional.Count < 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Użycie: dotnet script scan-workers.csx -- <KatalogDll> [<NazwaTypuDanych>] [--related]");
|
||||||
|
Console.Error.WriteLine("Przykład: dotnet script scan-workers.csx -- ./bin/Debug/net8.0");
|
||||||
|
Console.Error.WriteLine("Przykład: dotnet script scan-workers.csx -- ./bin/Debug/net8.0 DokumentHandlowy");
|
||||||
|
Console.Error.WriteLine("Przykład: dotnet script scan-workers.csx -- ./bin/Debug/net8.0 Pracownik --related");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllDir = Path.GetFullPath(positional[0]);
|
||||||
|
var typeFilter = positional.Count >= 2 ? positional[1] : null;
|
||||||
|
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("ScanWorkers")
|
||||||
|
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
|
||||||
|
.AddReferences(refs);
|
||||||
|
|
||||||
|
// Rekord opisujący pojedynczą rejestrację Worker/Extender (jednej klasie może
|
||||||
|
// odpowiadać wiele rejestracji — np. ten sam worker przypięty do różnych typów danych).
|
||||||
|
var registrations = new List<WorkerRegistration>();
|
||||||
|
|
||||||
|
foreach (var asmRef in compilation.References)
|
||||||
|
{
|
||||||
|
if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue;
|
||||||
|
foreach (var a in asm.GetAttributes())
|
||||||
|
{
|
||||||
|
var ac = a.AttributeClass;
|
||||||
|
if (ac == null) continue;
|
||||||
|
if (!IsWorkerAttribute(ac)) continue;
|
||||||
|
|
||||||
|
INamedTypeSymbol workerType = null;
|
||||||
|
INamedTypeSymbol dataType = null;
|
||||||
|
string alias = null;
|
||||||
|
|
||||||
|
// Wariant generyczny: [Worker<TWorker>] lub [Worker<TWorker, TData>]
|
||||||
|
if (ac.IsGenericType && ac.TypeArguments.Length >= 1)
|
||||||
|
{
|
||||||
|
workerType = ac.TypeArguments[0] as INamedTypeSymbol;
|
||||||
|
if (ac.TypeArguments.Length >= 2)
|
||||||
|
dataType = ac.TypeArguments[1] as INamedTypeSymbol;
|
||||||
|
|
||||||
|
// Opcjonalny string z konstruktora = alias (Name)
|
||||||
|
foreach (var arg in a.ConstructorArguments)
|
||||||
|
{
|
||||||
|
if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s)
|
||||||
|
{
|
||||||
|
alias = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wariant z parametrami: [Worker(typeof(TWorker))] / [Worker(typeof(TWorker), typeof(TData))]
|
||||||
|
// ewentualnie z dodatkowym name jako string.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ca = a.ConstructorArguments;
|
||||||
|
int typeIdx = 0;
|
||||||
|
foreach (var arg in ca)
|
||||||
|
{
|
||||||
|
if (arg.Kind == TypedConstantKind.Type && arg.Value is INamedTypeSymbol nt)
|
||||||
|
{
|
||||||
|
if (typeIdx == 0) workerType = nt;
|
||||||
|
else if (typeIdx == 1) dataType = nt;
|
||||||
|
typeIdx++;
|
||||||
|
}
|
||||||
|
else if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s && alias == null)
|
||||||
|
{
|
||||||
|
alias = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedArgument "Name" ma priorytet, gdy jest jawnie podany.
|
||||||
|
foreach (var na in a.NamedArguments)
|
||||||
|
{
|
||||||
|
if (na.Key == "Name" && na.Value.Value is string s) alias = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workerType == null) continue;
|
||||||
|
registrations.Add(new WorkerRegistration(workerType, dataType, alias, asm.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtr po nazwie typu danych — gdy podany drugi argument, ograniczamy do workerów
|
||||||
|
// przypiętych do tego typu (po prostej nazwie lub po pełnej nazwie z namespace).
|
||||||
|
// Z flagą --related dorzucamy typy powiązane: Row→Table (przez property `Table`),
|
||||||
|
// Table→Row (przez indekser this[int]), oraz history-row (gdy DataType implementuje
|
||||||
|
// IRowWithHistory — przez indekser this[Date]).
|
||||||
|
// Extendery (rejestracje bez DataType) są w trybie filtra pomijane.
|
||||||
|
|
||||||
|
var allowedDataTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
|
||||||
|
INamedTypeSymbol primaryFilterType = null;
|
||||||
|
if (typeFilter != null && includeRelated)
|
||||||
|
{
|
||||||
|
primaryFilterType = FindTypeByName(compilation, typeFilter);
|
||||||
|
if (primaryFilterType != null)
|
||||||
|
{
|
||||||
|
allowedDataTypes.Add(primaryFilterType);
|
||||||
|
var related = ResolveRelatedTypes(primaryFilterType).ToList();
|
||||||
|
foreach (var r in related) allowedDataTypes.Add(r);
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"# Typ podstawowy: {primaryFilterType.ToDisplayString()}");
|
||||||
|
foreach (var r in related)
|
||||||
|
Console.Error.WriteLine($"# Typ powiązany: {r.ToDisplayString()}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"# Nie znaleziono typu `{typeFilter}` w referencjach — --related wyłączony.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchesTypeFilter(INamedTypeSymbol dt)
|
||||||
|
{
|
||||||
|
if (typeFilter == null || dt == null) return typeFilter == null;
|
||||||
|
if (allowedDataTypes.Count > 0) return allowedDataTypes.Contains(dt);
|
||||||
|
return string.Equals(dt.Name, typeFilter, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(dt.ToDisplayString(), typeFilter, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered = typeFilter != null
|
||||||
|
? registrations.Where(r => r.DataType != null && MatchesTypeFilter(r.DataType)).ToList()
|
||||||
|
: registrations;
|
||||||
|
|
||||||
|
// Sortowanie: najpierw workery z dataType (po nazwie dataType), potem extendery (bez dataType).
|
||||||
|
var byData = filtered
|
||||||
|
.Where(r => r.DataType != null)
|
||||||
|
.GroupBy(r => r.DataType, SymbolEqualityComparer.Default)
|
||||||
|
.OrderBy(g => ((INamedTypeSymbol)g.Key).ToDisplayString(), StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var extenders = typeFilter == null
|
||||||
|
? registrations.Where(r => r.DataType == null)
|
||||||
|
.OrderBy(r => r.WorkerType.ToDisplayString(), StringComparer.Ordinal)
|
||||||
|
.ToList()
|
||||||
|
: new List<WorkerRegistration>();
|
||||||
|
|
||||||
|
WriteJson(byData, extenders, typeFilter);
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
static bool IsWorkerAttribute(INamedTypeSymbol attrClass)
|
||||||
|
{
|
||||||
|
// Nazwa klasy atrybutu w metadanych może mieć backtick dla wariantu generycznego
|
||||||
|
// (WorkerAttribute, WorkerAttribute`1, WorkerAttribute`2). Symbol.Name zwraca "WorkerAttribute"
|
||||||
|
// bez backticka, więc wystarczy porównać po nazwie i — dla bezpieczeństwa — sprawdzić namespace.
|
||||||
|
if (attrClass.Name != "WorkerAttribute") return false;
|
||||||
|
var ns = attrClass.ContainingNamespace?.ToDisplayString() ?? "";
|
||||||
|
return ns.StartsWith("Soneta", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasAttribute(ISymbol s, string attributeTypeName)
|
||||||
|
{
|
||||||
|
var shortName = attributeTypeName.EndsWith("Attribute")
|
||||||
|
? attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length)
|
||||||
|
: attributeTypeName;
|
||||||
|
foreach (var a in s.GetAttributes())
|
||||||
|
{
|
||||||
|
var n = a.AttributeClass?.Name;
|
||||||
|
if (n == attributeTypeName || n == shortName) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GetActionTitle(IMethodSymbol m)
|
||||||
|
{
|
||||||
|
foreach (var a in m.GetAttributes())
|
||||||
|
{
|
||||||
|
var n = a.AttributeClass?.Name;
|
||||||
|
if (n != "ActionAttribute" && n != "Action") continue;
|
||||||
|
foreach (var arg in a.ConstructorArguments)
|
||||||
|
{
|
||||||
|
if (arg.Kind == TypedConstantKind.Primitive && arg.Value is string s)
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return ""; // [Action] bez tytułu — i tak licz jako akcja
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string StripSuffix(string name, string suffix)
|
||||||
|
{
|
||||||
|
return name.EndsWith(suffix, StringComparison.Ordinal)
|
||||||
|
? name.Substring(0, name.Length - suffix.Length)
|
||||||
|
: name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteJson(
|
||||||
|
List<IGrouping<ISymbol, WorkerRegistration>> byData,
|
||||||
|
List<WorkerRegistration> extenders,
|
||||||
|
string typeFilter)
|
||||||
|
{
|
||||||
|
// Dictionary z zachowaną kolejnością wstawiania — opis na początku, potem klucze typów.
|
||||||
|
var root = new Dictionary<string, object>();
|
||||||
|
root["description"] = typeFilter != null
|
||||||
|
? $"Workery przypięte do typu `{typeFilter}` (Soneta)"
|
||||||
|
: "Workery i extendery (Soneta)";
|
||||||
|
|
||||||
|
foreach (var g in byData)
|
||||||
|
{
|
||||||
|
var dt = (INamedTypeSymbol)g.Key;
|
||||||
|
root[dt.ToDisplayString()] = g
|
||||||
|
.OrderBy(r => r.WorkerType.Name, StringComparer.Ordinal)
|
||||||
|
.Select(BuildWorkerJson)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
if (typeFilter == null && extenders.Count > 0)
|
||||||
|
{
|
||||||
|
root["__extenders__"] = extenders.Select(BuildWorkerJson).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
|
||||||
|
};
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(root, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, object> BuildWorkerJson(WorkerRegistration reg)
|
||||||
|
{
|
||||||
|
var w = reg.WorkerType;
|
||||||
|
var hasWorkerSuffix = w.Name.EndsWith("Worker", StringComparison.Ordinal);
|
||||||
|
var defaultAlias = hasWorkerSuffix
|
||||||
|
? StripSuffix(w.Name, "Worker")
|
||||||
|
: w.Name;
|
||||||
|
var aliasShown = !string.IsNullOrEmpty(reg.Alias) ? reg.Alias : defaultAlias;
|
||||||
|
|
||||||
|
var paramsList = new List<Dictionary<string, object>>();
|
||||||
|
|
||||||
|
// Parametry konstruktora (kind=ctor) — wybieramy pierwszy publiczny konstruktor z parametrami.
|
||||||
|
var ctor = w.InstanceConstructors
|
||||||
|
.FirstOrDefault(c => c.DeclaredAccessibility == Accessibility.Public && c.Parameters.Length > 0);
|
||||||
|
if (ctor != null)
|
||||||
|
{
|
||||||
|
foreach (var p in ctor.Parameters)
|
||||||
|
{
|
||||||
|
var entry = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = p.Name,
|
||||||
|
["type"] = p.Type.ToDisplayString(),
|
||||||
|
["kind"] = "ctor",
|
||||||
|
};
|
||||||
|
AttachContextBaseProps(entry, p.Type);
|
||||||
|
paramsList.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property z [Context] — inicjowane z Context.
|
||||||
|
foreach (var p in w.GetMembers().OfType<IPropertySymbol>()
|
||||||
|
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic && !p.IsIndexer)
|
||||||
|
.Where(p => HasAttribute(p, "ContextAttribute"))
|
||||||
|
.OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var entry = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = p.Name,
|
||||||
|
["type"] = p.Type.ToDisplayString(),
|
||||||
|
};
|
||||||
|
AttachContextBaseProps(entry, p.Type);
|
||||||
|
paramsList.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Akcje [Action] — metoda + tytuł + typ wyniku.
|
||||||
|
var actions = new List<Dictionary<string, object>>();
|
||||||
|
foreach (var m in w.GetMembers().OfType<IMethodSymbol>()
|
||||||
|
.Where(m => m.MethodKind == MethodKind.Ordinary
|
||||||
|
&& m.DeclaredAccessibility == Accessibility.Public
|
||||||
|
&& !m.IsStatic)
|
||||||
|
.OrderBy(m => m.Name, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var title = GetActionTitle(m);
|
||||||
|
if (title == null) continue;
|
||||||
|
actions.Add(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = title,
|
||||||
|
["method"] = m.Name,
|
||||||
|
["result"] = m.ReturnsVoid ? "void" : m.ReturnType.ToDisplayString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pozostałe public property z getterem (bez [Context]) — do bindowania / odczytu.
|
||||||
|
var props = new List<Dictionary<string, object>>();
|
||||||
|
foreach (var p in w.GetMembers().OfType<IPropertySymbol>()
|
||||||
|
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic && !p.IsIndexer)
|
||||||
|
.Where(p => p.GetMethod != null)
|
||||||
|
.Where(p => !HasAttribute(p, "ContextAttribute"))
|
||||||
|
.OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
props.Add(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = p.Name,
|
||||||
|
["type"] = p.Type.ToDisplayString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["workerAssembly"] = reg.AssemblyName,
|
||||||
|
["workerType"] = w.ToDisplayString(),
|
||||||
|
["name"] = aliasShown,
|
||||||
|
};
|
||||||
|
if (paramsList.Count > 0) obj["params"] = paramsList;
|
||||||
|
if (actions.Count > 0) obj["actions"] = actions;
|
||||||
|
if (props.Count > 0) obj["props"] = props;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dla typu parametru workera dziedziczącego z ContextBase doczepia listę publicznych
|
||||||
|
// instancyjnych property — to są pod-parametry, które użytkownik widzi w oknie parametrów workera.
|
||||||
|
static void AttachContextBaseProps(Dictionary<string, object> entry, ITypeSymbol type)
|
||||||
|
{
|
||||||
|
if (type is not INamedTypeSymbol nt) return;
|
||||||
|
if (!InheritsFromContextBase(nt)) return;
|
||||||
|
|
||||||
|
var props = new List<Dictionary<string, object>>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
for (var t = nt; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
if (t.Name == "ContextBase") break; // property bazowe ContextBase pomijamy (Context itp.)
|
||||||
|
foreach (var p in t.GetMembers().OfType<IPropertySymbol>())
|
||||||
|
{
|
||||||
|
if (p.DeclaredAccessibility != Accessibility.Public || p.IsStatic || p.IsIndexer) continue;
|
||||||
|
if (p.GetMethod == null) continue;
|
||||||
|
if (!seen.Add(p.Name)) continue;
|
||||||
|
props.Add(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["name"] = p.Name,
|
||||||
|
["type"] = p.Type.ToDisplayString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.Count > 0)
|
||||||
|
{
|
||||||
|
props = props.OrderBy(d => (string)d["name"], StringComparer.Ordinal).ToList();
|
||||||
|
entry["props"] = props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool InheritsFromContextBase(INamedTypeSymbol type)
|
||||||
|
{
|
||||||
|
for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
if (t.Name == "ContextBase"
|
||||||
|
&& (t.ContainingNamespace?.ToDisplayString() ?? "").StartsWith("Soneta", StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Szuka typu po prostej nazwie ("DokumentHandlowy") lub pełnej z namespace
|
||||||
|
// ("Soneta.Handel.DokumentHandlowy"). Zwraca pierwsze trafienie.
|
||||||
|
static INamedTypeSymbol FindTypeByName(CSharpCompilation compilation, string nameOrFullName)
|
||||||
|
{
|
||||||
|
foreach (var asmRef in compilation.References)
|
||||||
|
{
|
||||||
|
if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue;
|
||||||
|
foreach (var t in EnumerateAllTypes(asm.GlobalNamespace))
|
||||||
|
{
|
||||||
|
if (t.DeclaredAccessibility != Accessibility.Public) continue;
|
||||||
|
if (string.Equals(t.Name, nameOrFullName, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(t.ToDisplayString(), nameOrFullName, StringComparison.Ordinal))
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zbiór typów powiązanych z `primary` (przechodnio):
|
||||||
|
// - jeśli typ dziedziczy z `Soneta.Business.Row` → typ z property `Table` (klasa tabeli);
|
||||||
|
// - jeśli typ dziedziczy z `Soneta.Business.Table` → typ zwracany przez indekser `this[int]`
|
||||||
|
// (klasa rekordu);
|
||||||
|
// - jeśli typ implementuje interfejs `IRowWithHistory` → typ zwracany przez indekser
|
||||||
|
// przyjmujący `Soneta.Types.Date` (historyczny rekord).
|
||||||
|
// Reguły aplikowane są w pętli — np. dla `Pracownik` (Row + IRowWithHistory) najpierw
|
||||||
|
// dostajemy `Pracownicy` i `PracHistoria`, a `PracHistoria` (kolejny Row) dorzuca własną
|
||||||
|
// tabelę `PracHistorie`. Pętle są zabezpieczone zbiorem już odwiedzonych typów.
|
||||||
|
static IEnumerable<INamedTypeSymbol> ResolveRelatedTypes(INamedTypeSymbol primary)
|
||||||
|
{
|
||||||
|
var result = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
|
||||||
|
var queue = new Queue<INamedTypeSymbol>();
|
||||||
|
queue.Enqueue(primary);
|
||||||
|
var visited = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default) { primary };
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var t = queue.Dequeue();
|
||||||
|
foreach (var related in DirectRelatedTypes(t))
|
||||||
|
{
|
||||||
|
if (visited.Add(related))
|
||||||
|
{
|
||||||
|
result.Add(related);
|
||||||
|
queue.Enqueue(related);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<INamedTypeSymbol> DirectRelatedTypes(INamedTypeSymbol t)
|
||||||
|
{
|
||||||
|
if (InheritsFromNamed(t, "Row", "Soneta.Business"))
|
||||||
|
{
|
||||||
|
if (FindMemberInherited(t, m => m is IPropertySymbol p && !p.IsIndexer && p.Name == "Table")
|
||||||
|
is IPropertySymbol tableProp
|
||||||
|
&& tableProp.Type is INamedTypeSymbol tableType)
|
||||||
|
yield return tableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InheritsFromNamed(t, "Table", "Soneta.Business"))
|
||||||
|
{
|
||||||
|
if (FindMemberInherited(t, m => m is IPropertySymbol p
|
||||||
|
&& p.IsIndexer && p.Parameters.Length == 1
|
||||||
|
&& p.Parameters[0].Type.SpecialType == SpecialType.System_Int32)
|
||||||
|
is IPropertySymbol rowIndexer
|
||||||
|
&& rowIndexer.Type is INamedTypeSymbol rowType)
|
||||||
|
yield return rowType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImplementsInterface(t, "IRowWithHistory"))
|
||||||
|
{
|
||||||
|
if (FindMemberInherited(t, m => m is IPropertySymbol p
|
||||||
|
&& p.IsIndexer && p.Parameters.Length == 1
|
||||||
|
&& p.Parameters[0].Type is INamedTypeSymbol pt
|
||||||
|
&& pt.Name == "Date"
|
||||||
|
&& (pt.ContainingNamespace?.ToDisplayString() ?? "").StartsWith("Soneta", StringComparison.Ordinal))
|
||||||
|
is IPropertySymbol dateIndexer
|
||||||
|
&& dateIndexer.Type is INamedTypeSymbol histType)
|
||||||
|
yield return histType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool InheritsFromNamed(INamedTypeSymbol type, string name, string nsPrefix)
|
||||||
|
{
|
||||||
|
for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
if (t.Name == name
|
||||||
|
&& (t.ContainingNamespace?.ToDisplayString() ?? "").StartsWith(nsPrefix, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ImplementsInterface(INamedTypeSymbol type, string ifaceName)
|
||||||
|
{
|
||||||
|
return type.AllInterfaces.Any(i => i.Name == ifaceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ISymbol FindMemberInherited(INamedTypeSymbol type, Func<ISymbol, bool> predicate)
|
||||||
|
{
|
||||||
|
for (var t = type; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType)
|
||||||
|
{
|
||||||
|
var m = t.GetMembers().FirstOrDefault(predicate);
|
||||||
|
if (m != null) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
record WorkerRegistration(INamedTypeSymbol WorkerType, INamedTypeSymbol DataType, string Alias, string AssemblyName);
|
||||||
Reference in New Issue
Block a user