DSO.Core.Evoker
Reflection'ın esnekliği, derlenmiş kodun hızı, JSON'un taşınabilirliği — tek bir bağımlılıksız çekirdekte.
.NET'te isimle metot çağırmak, çalışma zamanında gerçek CLR tipleri üretmek, bu tiplerin üyelerine boxing'siz erişmek ve herhangi bir sınıfı JSON komutlarla kullanılabilir hale getirmek için yazılmış çekirdek kütüphane. Yalnızca System.Reflection.Emit, System.Linq.Expressions ve System.Text.Json kullanır.
İsimle metot
Overload seçimi, optional parametreler, async — tipli yolda 0 B allocation.
Gerçek CLR tipleri
Reflection.Emit ile milisaniyelerde, şema cache'li tip üretimi.
Boxing'siz erişim
Derlenmiş getter/setter'lar; materyalizasyonda ~194 kat hız.
JSON komutlar
Herhangi bir sınıfı kod değiştirmeden {"op":"invoke"} ile kullan.
Bir kural motoru, ORM materyalizasyonu, plugin sistemi, dinamik form/rapor üreticisi, uzaktan yönetim API'si ya da "bu DLL'deki şu metodu ekrandan çağırayım" ihtiyacı: hepsi aynı çekirdeğin üzerinde, aynı kurallarla çalışır.
Aile
Evoker çekirdeği tek başına kullanılabilir; ihtiyaç büyüdükçe aile paketleri aynı kurallar üzerine katman ekler.
Çağırma, tip üretimi, boxing'siz erişim, JSON komutlar, katalog, tip tanımı.
Üretilen tiplere interface / base class implementasyonu, generic metot, ref/out, event, indexer.
Üretilen tipleri System.Text.Json ile tek satırda serialize / deserialize.
Katalogdaki her hedefi ASP.NET Core uçlarından JSON komutla kullanma.
DLL plugin'leri ayrı process'te (sandbox) ya da unload edilebilir context'te çalıştırma, canlı mod geçişi.
Plugin yönetimini (tarama, kayıt, aktif/pasif, komut) hazır REST uçları olarak sunma.
Neden Evoker?
Çalışma zamanında "isimle bir şey yapmak" için yaygın yaklaşımların her biri bir bedel ödetir:
| Yaygın yaklaşım | Bedeli | Evoker'ın cevabı |
|---|---|---|
Klasik reflection (MethodInfo.Invoke) | Her çağrıda argüman dizisi, boxing, güvenlik kontrolleri; ~25 kat yavaş | Bir kez derlenen expression-tree delegate'leri; tipli sürümde 0 byte allocation |
dynamic / DLR | Gerçek tip yok; güçlü tipli API'lere (ORM, serializer, DI) verilemez | Gerçek CLR tipleri üretir; interface implement eder, her API'ye verilebilir |
| Property-bag dinamik nesneler | Interface yok, hücre başına allocation, tip güvenliği yok | IL ile üretilmiş gerçek property'ler, şema cache'i, tipli getter/setter |
| Interceptor'lı proxy çatıları | Her çağrı tek bir interceptor'dan geçer; ağır, ince kontrol zor | Metot başına bağımsız forwarder, metot başına ayrı delegate |
| "Önce derle, sonra çağır" üreteçleri | Derleyici bağımlılığı, saniyeler süren üretim, büyük bellek | Roslyn yok; Reflection.Emit ile milisaniyeler |
Sıradan kütüphanelerin genelde düşünmediği şeyler
- Instance-güvenli cache. Derlenmiş delegate'ler instance'ı içinde taşımaz. Aynı tip + metot için farklı nesnelerle çalışan iki builder birbirinin sonucunu asla ezemez.
- Akıllı overload seçimi. Önce birebir ad, sonra büyük/küçük harf duyarsız ad (VB.NET dostu); ardından argüman sayısı, optional parametreler ve tip uyumu. JSON komutlarda ayrıca sayısal tercih (int → long → …) ve
paramsdesteği. - Unload dostu. Her cache'in bir "unut" kapısı var (
ForgetType,ForgetAssembly); collectibleAssemblyLoadContext'ler gerçekten bellekten atılabilir. - JSON-native. Herhangi bir sınıf, değişiklik yapmadan JSON komutlarla kullanılabilir. Tanım (describe) ve hazır komut şablonları otomatik üretilir.
Rakamlar
Ölçümler TestKit içindeki PerfBench ile .NET 8, Release derlemesinde alındı. Mutlak değerler makineye göre değişir; oranlar belirleyicidir.
Çağırma — süre / çağrı
| Çağırma yolu | Süre / çağrı | Allocation / çağrı |
|---|---|---|
| Doğrudan derlenmiş, tipli delegate (teorik alt sınır) | 3.7 ns | 0 B |
MethodInfo.Invoke (klasik reflection) | 92.4 ns | 112 B |
EvokerBuilder.Invoke<int>("Add", a, b) | ~50 ns | 88 B* |
EvokerBuilder.GetFunc<int>(...) (önceden çözülmüş) | ~40 ns | 88 B* |
EvokerBuilder.GetFunc<int,int,int>("Add") | ~8–10 ns | 0 B |
* Çağıranın oluşturduğu object[] dizisi.
| Property / field erişimi | Süre | Allocation |
|---|---|---|
FieldInfo.GetValue (klasik reflection) | 76.3 ns | 24 B |
DynamicEntityAccessor getter delegate'i | 4.1 ns | 0 B |
EvokerBuilder.GetValue<int>("Counter") | 43 ns | 0 B |
| Materyalizasyon (200.000 satır) | Süre | Toplam alloc. | Satır başı |
|---|---|---|---|
| Reflection (Activator + isimle set) | 777 ms | 659 MB | ~3456 B |
DynamicEntityAccessor | 4 ms | 9 MB | ~48 B |
| Tip çözme | Süre |
|---|---|
EvokerEngine.ResolveType cache isabeti | 23 ns, 0 B |
| Bulunamayan ad (ilk sorgu: tüm assembly'ler taranır) | ~15 ms |
| Aynı bulunamayan ad tekrar (negatif cache) | ~45 µs |
Kurulum
Depoyu klonlayıp projenize referans olarak ekleyin:
| Hedef | .NET 6.0 ve üzeri |
| Bağımlılık | Yok — yalnızca BCL |
| Ad alanları | DSO.Core.Evoker, DSO.Core.Evoker.Commands, DSO.Core.Evoker.Conversion, DSO.Core.Evoker.Description |
60 saniyede Evoker
Dört temel yeteneğin her biri tek bir örnekte:
API Rehberi
Bu bölüm her sınıfın public yüzeyini örnekleriyle listeler; kütüphaneyi kullanırken başvuru kaynağı olarak kullanabilirsiniz.
EvokerEngine
statik giriş noktasıDSO.Core.EvokerTek satırlık çağrılar ve isimden tip çözme için.
ResolveType arama sırası
Bir seviyede birden fazla farklı tip eşleşirse (ör. iki plugin'de de Result sınıfı varsa) Evoker tahmin etmez: AmbiguousMatchException ile adayları listeler. Kısmen yüklenebilen assembly'ler (ReflectionTypeLoadException) aramayı düşürmez.
Collectible context'lerdeki tipler zayıf referansla cache'lenir: plugin unload'u engellenmez ve reload sonrası eski tip dönmez. Bulunamayan adlar da cache'lenir; yeni bir assembly yüklendiğinde bu cache kendiliğinden boşalır.
EvokerBuilder
isimle çağırmaDSO.Core.EvokerEsnek, genel amaçlı çağırma motoru. object[] argümanlarla çalışır; sıkı döngüler için tipli delegate'ler sunar.
Oluşturma ve instance modu
Çağırma
Metot seçme kuralları (FindMethod)
- İsim önce birebir aranır, bulunamazsa büyük/küçük harf duyarsız aranır (VB.NET'te
addileAddaynıdır). - Parametre sayısı argüman sayısına eşit olan adaylar önceliklidir. Yoksa fazlalık parametrelerinin hepsi optional olan adaylar kullanılır; eksikler metodun kendi varsayılan değerleriyle dolar (C#
= değer, VB.NETOptional). - Birden fazla aday kalırsa argüman tiplerine bakılır: önce birebir tip, sonra atanabilir tip.
- Uygun aday yoksa
MissingMethodExceptionfırlatılır; mesajda mevcut imzalar listelenir.
Aynı kuralları dışarıdan da kullanabilirsiniz:
Constructor seçimi
Constructor'lar metotlarla aynı kurallarla seçilir: sayı, optional, tip uyumu, sayısal genişletme (ör. int parametreye long). Değerler gerekirse EvokerValueConverter ile dönüştürülür. Tüm parametreleri optional olan bir constructor "parametresiz" sayılır.
Önceden çözülmüş delegate'ler (object[] tabanlı)
Tipli delegate'ler — boxing yok, allocation yok
Sıkı döngüler için en hızlı yol: argüman ve dönüş tipleri derleme zamanında bilinir. Son tip parametresi, .NET'in Func<> kuralındaki gibi dönüş tipidir.
- Overload,
T1..Tntiplerine göre seçilir. - Tip metodun parametresinden farklıysa (
int→long, türetilmiş → taban) dönüşüm derlenir; mümkün değilseInvalidCastExceptionfırlatılır. - Verilmeyen optional parametreler varsayılanla dolar.
SetInstanceverilmişse nesne delegate'e bağlanır; verilmemişse her çağrıda çözülür.- Sıfır argümanlı tipli sürüm bilerek yoktur; argümansız metotlarda
GetFunc<T>(ad)zaten boxing yapmaz.
Property ve field erişimi EvokerBuilderPropertyExtensions
GetValue/SetValue yalnızca SetInstance ile sabit bir nesneye bağlı builder'larda anlamlıdır; değilse InvalidOperationException fırlatılır.
Event'ler EvokerBuilderEventExtensions
Derleme zamanında delegate tipini bilmeden herhangi bir event'e abone olun. EventHandler, EventHandler<T>, özel delegate'ler ve VB.NET'in ürettiği gizli delegate'lerin hepsi desteklenir.
Static event'ler de desteklenir. Instance event'leri için builder'ın SetInstance ile bağlı olması gerekir.
Cache yönetimi
ref / out: object[] sözleşmesi bu değerleri çağırana geri taşıyamadığı için ref/out parametreli metotlar açık bir NotSupportedException ile reddedilir; değerler sessizce kaybolmaz. Bu tür metotlar için DSO.Core.Evoker.Extend ya da düz reflection kullanın.
DynamicEntityAccessor
sıfır allocation erişimORM satır okuma döngüsü gibi sıcak yollar için. Bilinen bir property, bilinen bir tiple, boxing olmadan okunur ve yazılır.
- Property yoksa aynı isimli field'a düşer.
includeNonPublic: trueile private/protected/internal üyeler de görülür.- Cache anahtarı struct'tır; cache isabetinde bile string üretilmez.
DynamicTypeFactory
çalışma zamanında tip üretimiGerçek CLR tipleri üretir. Tüm tipler tek, paylaşımlı bir modülde üretilir ve aynı şema ikinci kez istendiğinde IL yeniden üretilmez.
Genişletme parametreleri
Çekirdek bunlara ihtiyaç duymaz; Extend paketi kullanır.
configureType verildiğinde şema cache'i bilinçli olarak devre dışı kalır: aynı şema farklı yapılandırmalarla farklı tipler üretebilir. TypeMembers yapısı emisyonu biten property, metot, generic metot ve event builder'larını verir. InvokeGenericMethodDelegate generic forwarder'ların çağırdığı public yardımcıdır.
DynamicDelegateTypeFactory
her imzaya delegate tipiFunc<>/Action<>'ın ifade edemediği imzalar için (ref/out parametreler, 16'dan fazla parametre) gerçek bir delegate tipi üretir. Aynı imza tekrar istenirse aynı tip döner.
DynamicClass
hepsi tek bir akıştaDynamicTypeFactory + DynamicEntityAccessor + EvokerBuilder üçlüsünü tek bir akıcı (fluent) nesnede toplar. Thread-safe build sunar: aynı tanım birden fazla thread'den kurulsa bile tip ve instance yalnızca bir kez üretilir.
Oluşturma
Property'ler ve değerler
Değişiklik kancaları (instance'a özel)
Metotlar
Metot gövdesi bir delegate'tir; istediğiniz an değiştirebilirsiniz.
Generic metotlar (type-erasure)
Event'ler
Gerçek C# += söz dizimiyle abone olmak için tipi bir interface'e bağlayın: Extend → Implement<T>.
Attribute ekleme
Yaşam döngüsü ve genişletme
Cache ömrü: Üretilen tip ve derlenmiş accessor'lar paylaşımlı, process ömürlü cache'lere aittir. Her seferinde farklı kolon seti üreten senaryolarda useSchemaCache: false, forgetOnDispose: true kullanıp using ile temizleyin.
Commands
JSON komutlarDSO.Core.Evoker.CommandsHerhangi bir sınıfı kodunda hiçbir değişiklik yapmadan JSON ile kullanılabilir hale getirir. Aynı komut biçimi her yerde çalışır: bir tipte (EvokerTarget), bir plugin'de (PluginTarget, sandbox worker'ın içinde bile) ve web üzerinden (DSO.Core.Evoker.Api).
Komut biçimi
Alan adları büyük/küçük harf duyarsızdır. op verilmezse invoke kabul edilir.
Argüman bağlama kuralları
- Sıralı: argüman sayısı ≤ parametre sayısı olmalı; kalan parametreler optional olmalı.
- İsimli: her ad bir parametreye karşılık gelmeli; verilmeyen parametreler optional olmalı.
- Tek parametreli metot: isimli nesne hiçbir adayla eşleşmezse, nesnenin kendisi o tek parametrenin değeri sayılır (
SaveCustomer(Customer c)'ye doğrudan{ "Code": "C1" }). - Puanlama: adaylar JSON değer türü uyumuna göre puanlanır. Tam sayı önce int, sonra long ve diğerleri; ondalık önce double, sonra decimal, sonra float. Değer tipe sığmalıdır (300
byte'a gitmez). Metin; string, tarih, Guid veya enum adı olabilir. Nesne, sınıf ya da dictionary olabilir. - params / ParamArray: argümanlar tek tek de verilebilir (
["A", "B"]); hiç verilmezse boş dizi geçer. - Belirsizlik: eşit puanlı adaylar kalırsa
AmbiguousMatchdöner veargTypesile belirtmeniz istenir.
Komutları koddan üretmek
EvokerTarget ve nesne ömrü
EvokerLifetime, .NET'in DI ömürleriyle aynı mantıkta çalışır:
| Ömür | Davranış |
|---|---|
Singleton (varsayılan) | Tek nesne; durum komutlar arasında korunur |
Scoped | Komut başına bir nesne; çok adımlı komutun adımları aynı nesneyi paylaşır |
Transient | Her adımda yeni nesne |
Static | Nesne yok; static sınıflar otomatik olarak bu ömrü alır |
Özellikler: Type, Lifetime, IncludeNonPublic, Kind ("Type" / "Instance"), TypeFullName, Instance. Kendi hedef türünüzü yazmak için IEvokerTarget arayüzünü (Kind, TypeFullName, ExecuteAsync, DescribeAsync) uygulayın; Plugins paketindeki PluginTarget bu arayüzü kullanır.
EvokerCatalog — Guid anahtarlı hedef listesi
İsimle erişim ve izin listesi: Web'e açılan senaryolar için kayıt gerektirmeyen tipler sadece izin verilen kalıplarla açılır. Varsayılan kapalıdır.
Sonuç ve hata modeli
Komutlar exception fırlatmaz; hatalar sonucun içinde döner. Bu yüzden web'e doğrudan verilebilir.
| Tip | Üyeler |
|---|---|
EvokerCommandResult | Success, Result, Steps, Error, ElapsedMs, Mode, ToJson(indented), FromJson(json), Fail(...), Options |
EvokerStepResult | Index, Op, Member, As, Success, Result, Error, ElapsedMs, Skipped |
EvokerError | Code, Message, ExceptionType, StepIndex, BatchIndex, Member |
EvokerCommandException | Code, TargetExceptionType, BatchIndex (kod içinden fırlatmak için) |
EvokerErrorCodes | HTTP | Anlamı |
|---|---|---|
BadRequest | 400 | Komut okunamadı / geçersiz |
NotAllowed | 403 | İzin listesi dışında |
TargetNotFound | 404 | Hedef yok |
MemberNotFound | 404 | Üye yok |
Inactive | 409 | Hedef pasif (plugin) |
InvalidArguments | 422 | Argümanlar hiçbir imzaya uymuyor |
AmbiguousMatch | 422 | Birden fazla imza eşit uyuyor |
InvalidOperation | 422 | İşlem bu hedefte yapılamaz |
Cancelled | 499 | İstek iptal edildi |
TargetException | 500 | Hedefin kendi kodu exception fırlattı |
InternalError | 500 | Beklenmeyen hata |
Timeout | 504 | timeoutMs aşıldı |
HTTP karşılığı EvokerErrorCodes.HttpStatus(code) ile alınır.
Conversion
değer dönüştürmeDSO.Core.Evoker.ConversionTüm katmanların ortak dönüştürücüsü.
ConvertTo, aynı şekle sahip iki farklı tip arasında (ör. plugin'in Point'i ile host'un PointDto'su) dönüşümü dahili, derlenmiş bir kopyalayıcıyla yapar. Bu kopyalayıcı sonucu JSON yoluyla birebir aynı olan şekillerde kullanılır; diğer durumlarda JSON'a düşülür.
Unload kapıları: EvokerValueConverter.ForgetAssembly(asm) ve EvokerJson.ForgetAssembly(asm) çağrıları, System.Text.Json'ın global cache'leri dahil, bir assembly'ye ait tüm metadata'yı bırakır.
Description
tip tanımı ve şablonlarDSO.Core.Evoker.DescriptionBir tipin tam tanımını ve her üye için hazır JSON komut şablonlarını üretir. Hem çalışan tiplerle hem de DLL'i çalıştırmadan (MetadataLoadContext tipleriyle) çalışır.
Tanım modeli
FullName, Name, Namespace, AssemblyName, Kind, BaseType, Interfaces, Constructors, Methods, Properties, Fields, Events, Values, Warnings
Ad, görünürlük, dönüş tipi; parametreler (yön, optional, varsayılan, params); IsStatic/IsAsync/IsVirtual/IsAbstract/IsOverride; DeclaredIn; okunur Signature; NotCallableReason; hazır Sample.
Get ve set için ayrı görünürlük (ör. Getter=Public, Setter=Private); init, static ve indexer bilgisi; o anki Value; Sample ve SampleSet şablonları.
Class, StaticClass, AbstractClass, Record, Struct, Enum, Interface, Delegate
Örnek imza çıktısı: "Task<int> AddAsync(int a, int b = 5)"
Plugin ve unload dostu tasarım
Collectible AssemblyLoadContext'ler, içlerindeki bir tipe güçlü referans tutan tek bir statik cache yüzünden bile bellekten atılamaz. Evoker'daki her cache'in bu yüzden bir çıkış kapısı vardır:
| Cache | Temizleme |
|---|---|
| Derlenmiş metot delegate'leri | EvokerBuilder.ForgetType(type) |
| Accessor ve constructor'lar | DynamicEntityAccessor.ForgetType(type) / builder.ForgetCache() |
| Komut üye cache'i | EvokerBuilder.ForgetType ile birlikte otomatik |
| Tip adı cache'i | Collectible tipler zaten zayıf referanslı; context unload olunca kendiliğinden silinir |
| JSON / şekil eşleme | EvokerJson.ForgetAssembly(asm), EvokerValueConverter.ForgetAssembly(asm) |
DSO.Core.Evoker.Plugins bunların hepsini unload sırasında kendisi çağırır. UnloadTest paketi, in-process bir plugin'in Evoker ile defalarca kullanıldıktan sonra gerçekten bellekten atıldığını doğrular.
Testler ve örnekler
| Proje | İçerik |
|---|---|
DSO.Core.Evoker.TestRunner | Çekirdek + Extend test koşucusu (228 kontrol). BuilderFeatureTests (F1–F8), CommandTests (C1–C13), ExtensionPointTests (E1–E4) ve Extend testleri. |
TestKit/PerfBench | Bu sayfadaki tüm performans rakamlarının kaynağı. |
DSO.Core.Evoker.Plugins.DemoApi | Katalog hedeflerinin (Singleton/Scoped/Transient/Static) web üzerinden kullanımı — demo.http. |
DSO.Core.Evoker.TestApi | Depodaki örnek ASP.NET Core API projesi. |
Beklenen çıktı: her bölüm TÜM ... TESTLERİ GEÇTİ ✅ ile biter.
Bilinen sınırlar
- Şema ve accessor cache üst sınırları FIFO tahliye kullanır; gerçek LRU değildir.
ForgetType/ForgetSchemakendi cache'lerimizi temizler.DynamicTypeFactory'nin ürettiği tipler collectible değildir (AssemblyBuilderAccess.Run); metadata'ları process ömrü boyunca kalır.EvokerBuilderileref/outparametreli metot çağrılmaz; bunun yerine açık bir hata verilir.- Üretilen tiplerin
ToString/Equals/GetHashCodedavranışıobject'ten gelir.