14 KiB
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:
[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:
- Zbierz wszystkie
*.dllz podanego katalogu, dodaj jakoMetadataReference. 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. - Zbuduj
CSharpCompilationz tymi referencjami. - Dla każdego
IAssemblySymbolprzejdź po atrybutach assembly (asm.GetAttributes()) i odfiltruj te, których klasa toWorkerAttributez namespace zaczynającego się odSoneta(chroni przed kolizją z atrybutami o tej samej nazwie z innych bibliotek). - Wyciągnij dane rejestracji w zależności od wariantu atrybutu:
- wariant generyczny (
[Worker<TWorker>],[Worker<TWorker, TData>]) — typy biorę zAttributeClass.TypeArguments[0](worker) orazTypeArguments[1](data, opcjonalnie). W metadanych klasa atrybutu ma backtick (WorkerAttribute\1,WorkerAttribute`2), aleINamedTypeSymbol.Namezwraca"WorkerAttribute"` bez backticka — wystarczy porównanie po nazwie. - wariant z parametrami (
[Worker(typeof(TWorker), typeof(TData))]) — typy biorę zConstructorArguments(TypedConstantKind.Type). Dodatkowy string z konstruktora lubNamedArgumentNametraktuję jako alias bindingu (nadpisuje nazwę domyślną).
- wariant generyczny (
- Pogrupuj rejestracje wg
DataType(workery) i posortuj alfabetycznie. Rejestracje bezDataTypetrafiają pod osobny klucz__extenders__w JSON-ie. - Dla każdej klasy workera / extendera odczytaj:
- Alias bindingu (pole
name) — jawnie podanyNamez atrybutu albo nazwa klasy bez sufiksuWorker/ pełna nazwa dla extendera. Binding na UI:- worker (lista):
{Workers.<name>.<Property>} - extender (formularz):
{new <name>.<Property>}
- worker (lista):
- Konstruktor inicjowany z Context — parametry pierwszego publicznego konstruktora
z parametrami trafiają do
paramszkind: "ctor". - Property z
[Context]— publiczne instancyjne property z atrybutemContextAttribute, inicjowane z Context — trafiają doparamsbez polakind. - Pod-parametry typu
ContextBase— gdy typ parametru dziedziczy zSoneta.Business.ContextBase, wpis dostaje zagnieżdżonepropsz publicznymi property tej klasy (pomija property samegoContextBase). - Property do bindowania / odczytu (
propsworkera) — pozostałe publiczne instancyjne property z getterem (bez[Context]). - Akcje menu Czynności (
actions) — publiczne instancyjne metody z atrybutemActionAttribute; każda akcja maname(tytuł),method(nazwa metody),result(typ wyniku,voiddlavoid).
- Alias bindingu (pole
- Wypisz JSON na stdout (sformatowany, z polskimi znakami w czystej formie). Sekcje
params/actions/propssą pomijane, gdy są puste.
Wymagania
- .NET SDK (8.0+)
dotnet-script:dotnet tool install -g dotnet-script
Uruchomienie
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ą przechodnio i łącznie — np. dla Pracownik (Row + IRowWithHistory)
zestaw to: Pracownik, Pracownicy, PracHistoria, PracHistorie (tabela historii dochodzi,
bo PracHistoria to też Row z własną Table).
Dodatkowo dla każdego znalezionego Row (oryginał + history-Row) skrypt rozszerza zestaw o całą hierarchię:
- klasy bazowe (
baseClasses) — chodzi w górę poBaseTypeaż doobjectwyłącznie (włącznie z generowanym*Rowz*Module, frameworkowymiRow/GuidedRow/RowBase); - klasy pochodne (
derivedClasses) — przeszukuje wszystkie referencje w poszukiwaniu klas mających dany Row w łańcuchuBaseType(np.OsobaWspolpracujaca,PracownikFirmy,WlascicieldlaPracownik).
Tabele tego rozszerzenia nie dostają — zbędne, intermediate *Table rzadko bywa celem rejestracji
workera.
Informacje o znalezionych typach skrypt wypisuje dwojako:
-
W JSON pod kluczem
scope(na stdout, sformatowane do parsowania):{ "description": "Workery przypięte do typu `Pracownik` (Soneta)", "scope": { "primary": "Soneta.Kadry.Pracownik", "related": [ { "type": "Soneta.Kadry.Pracownicy", "kind": "table" }, { "type": "Soneta.Kadry.PracHistoria", "kind": "history-row" }, { "type": "Soneta.Kadry.PracHistorie", "kind": "history-table" } ], "baseClasses": [ "Soneta.Kadry.KadryModule.PracownikRow", "Soneta.Business.GuidedRow", "Soneta.Business.Row", "Soneta.Kadry.KadryModule.PracHistoriaRow" ], "derivedClasses": [ "Soneta.Kadry.OsobaWspolpracujaca", "Soneta.Kadry.PracownikFirmy", "Soneta.Kadry.Wlasciciel" ] }, "Soneta.Kadry.Pracownik": [ /* … */ ], "Soneta.Kadry.Pracownicy": [ /* … */ ] }Pole
scopepojawia się wyłącznie w trybie--related(przy zwykłym filtrze JSON nie ma tego klucza). Dozwolone wartościscope.related[].kind:table,row,history-row,history-table. -
W logu na stderr (
# Typ podstawowy: …,# Typ powiązany (kind): …,# Klasa bazowa: …,# Klasa pochodna: …) — wygodne do szybkiego podglądu w konsoli.
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
(bez sekcji scope).
Przykłady
Pełna inwentaryzacja:
dotnet script ~/.claude/skills/soneta-programming/scripts/scan-workers.csx \
-- ./bin/Debug/net8.0
Tylko workery przypięte do DokumentHandlowy:
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:
{
"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żdyDataType(pełna nazwa z namespace). paramsłączy parametry konstruktora (zkind: "ctor") oraz property z atrybutem[Context](bez polakind) — wszystko, co Soneta inicjuje zContextprzy tworzeniu workera.- Gdy typ parametru dziedziczy z
Soneta.Business.ContextBase(klasa parametrów workera — zwykle nestedParamsw klasie workera), wpis zawiera dodatkowopropsz listą publicznych, instancyjnych property tej klasy. To pod-parametry, które użytkownik widzi w oknie parametrów workera. Property samegoContextBase(np.Context) są pomijane. actions— metody z atrybutem[Action].nameto tytuł z atrybutu,methodto nazwa metody w C#,resultto deklarowany typ wyniku (voiddla metod bez wartości).props— publiczne, instancyjne property z getterem, bez[Context]— kandydaci do bindowania wform.xmlprzez{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
# 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
WorkerAttributepo nazwie i namespaceSoneta*. 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
internalnie 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
- Inwentaryzacja workerów — uruchom
scan-workers.csx, znajdź wszystkie workery zarejestrowane dla interesującego Cię typu danych (np.DokumentHandlowy). - Wybór aliasu do bindingu — z sekcji workera odczytaj
AliasiProperty do bindowania— to bezpośrednio wartości do podstawienia wform.xml:{Workers.<Alias>.<Property>}(worker) lub{new <Alias>.<Property>}(extender). - Lista akcji — kolumna „Menu Czynności" pokazuje, które pozycje pojawią się w menu Czynności dla danego obiektu.
- 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 — semantyka workerów/extenderów, atrybut
[Context],[Action], metody sterująceIsVisibleXxx/IsEnabledXxx/GetNameXxx/IsCheckedXxx, bindowanie w form.xml. - context.md — jak działa
Contexti co może być źródłem parametrów workera. - scan-modules.md — lista modułów i tabel platformy (komplementarne do scan-workers).
- scan-props.md — pola konkretnego rekordu (do których workery doklejają property kalkulowane).