Proxy Pattern: Lazy Loading, Remote Proxy ve DispatchProxy ile Araya Girmek
Proxy, gerçek object ile aynı interface'i implement edip ona erişimi kontrol eder: oluşturmayı erteler (virtual), yetki kontrol eder (protection), uzaktaki bir servisi temsil eder (remote) ya da erişimde ek iş yapar (smart reference). EF Core'un Castle.Proxies.AuthorProxy'si ve gizlediği N+1, Lazy<T> ile virtual proxy, typed HttpClient'ın remote proxy olması, DispatchProxy'nin async ve TargetInvocationException tuzakları ve Decorator ile farkı.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un yazar sayfası Entity Framework Core (EF Core) ile SQLite'tan okuyor ve ekip bir süre önce UseLazyLoadingProxies()'i açmış; "Include yazmayı unutsak da çalışsın" diye. Model sıradan; tek fark navigation property'lerin virtual olması:
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Bookstore.Data;
public class Author
{
public int Id { get; set; }
public required string Name { get; set; }
// virtual: proxy bu property'yi override edip ilk erişimde sorgu atar
public virtual ICollection<Book> Books { get; set; } = [];
}
public class Book
{
public int Id { get; set; }
public required string Title { get; set; }
public int AuthorId { get; set; }
public virtual Author Author { get; set; } = null!;
}
public sealed class BookstoreContext(SqliteConnection connection) : DbContext
{
public int Commands { get; set; }
public DbSet<Author> Authors => Set<Author>();
public DbSet<Book> Books => Set<Book>();
protected override void OnConfiguring(DbContextOptionsBuilder options) =>
options
.UseSqlite(connection)
.UseLazyLoadingProxies()
.LogTo(_ => Commands++, [RelationalEventId.CommandExecuted]);
}
Veritabanında beş yazar ve her birinin iki kitabı var. Aşağıdaki kod yazarları tek sorguyla okuyor ve her birinin kitap sayısını yazıyor. Bu blok boyunca veritabanına kaç SQL (Structured Query Language) sorgusu gider? Ve authors[0].GetType() ne döndürür?
using (BookstoreContext db = new(connection))
{
List<Author> authors = db.Authors.ToList();
Console.WriteLine($"entity type: {authors[0].GetType().FullName}");
// Masum görünen döngü: her yazarın Books'u ilk erişimde ayrı bir sorgu
foreach (Author author in authors)
Console.WriteLine($" {author.Name}: {author.Books.Count} books");
Console.WriteLine($"lazy: {db.Commands} SQL commands");
}
Altı Sorgu ve Castle.Proxies.AuthorProxy
entity type: Castle.Proxies.AuthorProxy
Frank Herbert: 2 books
Ursula K. Le Guin: 2 books
Isaac Asimov: 2 books
Octavia E. Butler: 2 books
Iain M. Banks: 2 books
lazy: 6 SQL commands
Bir sorgu yazarlar için, beş sorgu da her yazarın Books'u için: klasik N+1. Daha ilginç olan ilk satır. db.Authors.ToList() sana Author değil, runtime'da üretilmiş Castle.Proxies.AuthorProxy tipinde object'ler döndürdü. Bu tip Author'dan türüyor, virtual olan Books property'sini override ediyor ve ilk erişimde, collection'ı döndürmeden önce context'e "bu navigation'ı yükle" diyor. Kodun Author ile konuştuğunu sanıyor; aslında arada bir proxy var ve bu proxy her erişimi bir veritabanı çağrısına çevirebiliyor.
Aynı veriyi Include ile tek sorguda almak mümkün:
using (BookstoreContext db = new(connection))
{
List<Author> authors = db.Authors.Include(a => a.Books).ToList();
int total = authors.Sum(a => a.Books.Count);
Console.WriteLine($"include: {db.Commands} SQL command(s), {total} books");
}
include: 1 SQL command(s), 10 books
Bu yazının konusu tam olarak bu ara katman: gerçek object'in önüne aynı interface'le geçen ve ona erişimi kontrol eden Proxy pattern'i. Türlerini (virtual, protection, remote), .NET'te runtime'da proxy üretmenin yolu olan DispatchProxy'yi ve iki tuzağını, Proxy'nin Decorator'dan farkını ve proxy'nin gizlediği maliyeti konuşacağız.
GoF'un Tanımı ve Proxy Türleri
Proxy, GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson ve John Vlissides'in 1994 tarihli Design Patterns: Elements of Reusable Object-Oriented Software kitabı) kataloğunun structural pattern'lerinden biri. Amacı, kendi cümlelerimle: bir object'e erişimi kontrol etmek için onun yerine geçen (surrogate) bir object sağlamak. Proxy, gerçek subject ile aynı interface'i implement eder; client farkı görmez. Roller üç tane: Subject (ortak interface), RealSubject (gerçek işi yapan object) ve Proxy (RealSubject'e referans tutan, erişimi kontrol eden ve gerektiğinde onu oluşturup silmekten sorumlu olan object).
GoF dört tür sayar. Remote proxy, başka bir adres alanındaki (başka bir process, başka bir makine) object'i yerelde temsil eder. Virtual proxy, oluşturulması pahalı bir object'i gerçekten gerekene kadar oluşturmaz; kitabın örneği, belge açılırken büyük görselleri yüklemeyen bir image proxy. Protection proxy, erişim haklarına göre çağrıyı geçirir ya da reddeder. Smart reference ise erişim sırasında ek iş yapan bir referanstır; kitabın saydığı örneklerden biri, kalıcı bir object'i ilk referans verildiğinde belleğe yüklemek. EF Core'un lazy-loading proxy'si tam olarak bu: navigation'a ilk erişimde veriyi veritabanından getiren bir smart reference.
Virtual Proxy: Lazy<T> ile Geç Oluşturmak
Yıllık satış raporu motoru açılışta template'leri, font'ları ve kur tablosunu yüklüyor. Uygulamanın çoğu isteği bu motora hiç ihtiyaç duymuyor, ama DI (Dependency Injection) container'ı onu ilk enjekte edildiği anda oluşturuyor. Virtual proxy, motoru ilk Render çağrısına kadar erteliyor:
namespace Bookstore.Reports;
public interface ISalesReportEngine
{
string Render(int year);
}
// Gerçek subject: oluşturulması pahalı (template'ler, font'lar, kur tablosu)
public sealed class SalesReportEngine : ISalesReportEngine
{
public SalesReportEngine()
{
Console.WriteLine(" [engine] loading templates and exchange rates...");
Thread.Sleep(300);
}
public string Render(int year) => $"Sales report {year}";
}
// Virtual proxy: gerçek object'i ilk gerçek ihtiyaçta, bir kez oluşturur
public sealed class LazySalesReportEngine(Func<SalesReportEngine> create) : ISalesReportEngine
{
private readonly Lazy<SalesReportEngine> _engine = new(create);
public string Render(int year) => _engine.Value.Render(year);
}
public static class VirtualProxyDemo
{
public static void Run()
{
Console.WriteLine("app started");
ISalesReportEngine engine = new LazySalesReportEngine(() => new SalesReportEngine());
Console.WriteLine("proxy registered");
Console.WriteLine(engine.Render(2025));
Console.WriteLine(engine.Render(2026));
}
}
app started
proxy registered
[engine] loading templates and exchange rates...
Sales report 2025
Sales report 2026
Proxy'nin oluşturulması bedava; pahalı constructor yalnızca ilk Render'da ve bir kez çalışıyor. Lazy<T> varsayılan olarak thread-safe (LazyThreadSafetyMode.ExecutionAndPublication), yani iki istek aynı anda gelse de motor bir kez oluşur. Bu proxy'yi DI'a services.AddSingleton<ISalesReportEngine>(sp => new LazySalesReportEngine(() => new SalesReportEngine())) gibi bir factory ile kaydedersin; client'lar ISalesReportEngine ister ve proxy'den haberleri olmaz. Not: client'ın kendisi Lazy<ISalesReportEngine> ya da Func<ISalesReportEngine> enjekte etseydi de aynı geç oluşturmayı alırdın, ama o zaman "bu dependency pahalı" bilgisi her client'a sızardı. Proxy bu bilgiyi tek yerde tutuyor.
Protection Proxy: Aynı Interface, Önce Yetki
İade işlemi finans ekibine açık; destek ekibi 50 EUR'ya kadar iade yapabiliyor, depo ekibi hiç yapamıyor. Kuralı RefundService'in içine yazmak, iadeyi yetki kavramına bağlar; proxy ise kontrolü önüne koyar:
namespace Bookstore.Payments;
public interface IRefundService
{
void Refund(Guid orderId, decimal amount);
}
public sealed class RefundService : IRefundService
{
public void Refund(Guid orderId, decimal amount) => Console.WriteLine($" refunded {amount} EUR");
}
public sealed record CurrentUser(string Name, string Role);
// Protection proxy: aynı interface, önce yetki kontrolü
public sealed class AuthorizedRefundService(IRefundService inner, CurrentUser user) : IRefundService
{
private const decimal SupportLimit = 50m;
public void Refund(Guid orderId, decimal amount)
{
bool allowed = user.Role switch
{
"Finance" => true,
"Support" => amount <= SupportLimit,
_ => false
};
if (!allowed)
throw new UnauthorizedAccessException($"{user.Name} ({user.Role}) cannot refund {amount} EUR.");
inner.Refund(orderId, amount);
}
}
public static class ProtectionProxyDemo
{
public static void Run()
{
foreach (CurrentUser user in (CurrentUser[])[new("Ada", "Finance"), new("Alan", "Support"), new("Grace", "Warehouse")])
{
IRefundService refunds = new AuthorizedRefundService(new RefundService(), user);
try
{
refunds.Refund(Guid.CreateVersion7(), 80m);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($" denied: {ex.Message}");
}
}
}
}
refunded 80 EUR
denied: Alan (Support) cannot refund 80 EUR.
denied: Grace (Warehouse) cannot refund 80 EUR.
Bir Web API'de (Application Programming Interface) bu kontrolün doğal yeri genellikle endpoint'tir: [Authorize], policy'ler ve resource-based authorization (yetkilendirme), ASP.NET Core Web API serisindeki "Authorization: Roller, Policy'ler ve Resource-Based Yetkilendirme" yazısında anlattığımız gibi. Protection proxy, aynı service'in HTTP (Hypertext Transfer Protocol) dışından da çağrıldığı yerlerde değer kazanır: bir background job, bir mesaj tüketicisi ya da bir yönetim konsolu da IRefundService kullanıyorsa, kural bütün girişlerde aynı kalır.
Remote Proxy: Typed HttpClient ve gRPC Client'ları
Remote proxy'yi .NET'te muhtemelen her gün yazıyorsun, adını koymadan. Katalog ayrı bir servise taşındığında, client kodun değişmesin diye aynı ICatalog interface'ini HTTP üzerinden implement eden bir typed client yazarsın:
using System.Net.Http.Json;
namespace Bookstore.Catalog;
public sealed record Book(string Isbn, string Title, decimal Price);
public interface ICatalog
{
Task<Book?> FindAsync(string isbn, CancellationToken ct = default);
Book? FindCached(string isbn);
}
// Yerel implementation
public sealed class InMemoryCatalog : ICatalog
{
private readonly Dictionary<string, Book> _books = new()
{
["978-0441013593"] = new("978-0441013593", "Dune", 12.99m)
};
public async Task<Book?> FindAsync(string isbn, CancellationToken ct = default)
{
await Task.Delay(120, ct);
return _books.GetValueOrDefault(isbn);
}
public Book? FindCached(string isbn) =>
_books.TryGetValue(isbn, out Book? book) ? book : throw new InvalidOperationException($"Unknown ISBN {isbn}");
}
// Remote proxy: aynı interface, her çağrı bir HTTP isteği
public sealed class CatalogClient(HttpClient http) : ICatalog
{
public Task<Book?> FindAsync(string isbn, CancellationToken ct = default) =>
http.GetFromJsonAsync<Book>($"books/{isbn}", ct);
public Book? FindCached(string isbn) => throw new NotSupportedException("Use FindAsync over the network.");
}
public static class RemoteProxyDemo
{
public static async Task RunAsync()
{
CountingHandler handler = new();
ICatalog catalog = new CatalogClient(new HttpClient(handler) { BaseAddress = new Uri("https://catalog.example/") });
// Client kodu yerel bir koleksiyonla konuşuyor gibi görünüyor
string[] cart = ["978-0441013593", "978-0441013593", "978-0441013593"];
decimal total = 0;
foreach (string isbn in cart)
total += (await catalog.FindAsync(isbn))?.Price ?? 0;
Console.WriteLine($"total {total} EUR, HTTP requests: {handler.Requests}");
}
// Ağa çıkmadan cevap veren ve istekleri sayan handler
private sealed class CountingHandler : HttpMessageHandler
{
public int Requests { get; private set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
Requests++;
await Task.Delay(50, ct);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""{"isbn":"978-0441013593","title":"Dune","price":12.99}""", Encoding.UTF8, "application/json")
};
}
}
}
total 38.97 EUR, HTTP requests: 3
Kod üç kez aynı kitabın fiyatını soruyor ve üç HTTP isteği gidiyor. Yerel InMemoryCatalog ile bu döngü zararsızdı; remote proxy arkasında her iterasyon bir network gidiş dönüşü, bir timeout ihtimali ve bir hata türü daha. Proxy'nin gücü olan şeffaflık burada zayıflığı: interface aynı olduğu için client kodu bedeli göremiyor. gRPC (Google'ın geliştirdiği, açık kaynak bir RPC (Remote Procedure Call) framework'ü) için Grpc.Tools'un .proto dosyasından ürettiği client class'ı da aynı yapıda bir remote proxy: service method'larının birebir karşılığı olan method'lar ve arkada bir network çağrısı. İyi bir remote proxy bu bedeli imzasıyla belli eder: method'lar async olur, CancellationToken alır ve mümkünse toplu çağrı sunar (FindManyAsync(isbns)).
DispatchProxy ile Runtime'da Proxy Üretmek
Her interface için elle proxy yazmak, tek bir kesişen ihtiyaç (süre ölçmek, loglamak) için tekrar eden kod demek. .NET'in System.Reflection.DispatchProxy class'ı, verdiğin interface'i runtime'da implement eden bir tip üretir ve her method çağrısını senin Invoke override'ına yönlendirir. İlk deneme çoğu zaman böyle görünür:
using System.Diagnostics;
using System.Reflection;
using System.Runtime.ExceptionServices;
namespace Bookstore.Diagnostics;
// İlk deneme: senkron düşünülmüş bir proxy
public class NaiveTimingProxy<T> : DispatchProxy where T : class
{
private T _target = null!;
public static T Create(T target)
{
T proxy = Create<T, NaiveTimingProxy<T>>();
((NaiveTimingProxy<T>)(object)proxy)._target = target;
return proxy;
}
protected override object? Invoke(MethodInfo? method, object?[]? args)
{
Stopwatch watch = Stopwatch.StartNew();
object? result = method!.Invoke(_target, args);
Console.WriteLine($" [timing] {method.Name}: {watch.ElapsedMilliseconds} ms");
return result;
}
}
// Düzeltilmiş hâli: süreyi Task bitince yazar, reflection'ın sardığı exception'ı açar
public class TimingProxy<T> : DispatchProxy where T : class
{
private T _target = null!;
public static T Create(T target)
{
T proxy = Create<T, TimingProxy<T>>();
((TimingProxy<T>)(object)proxy)._target = target;
return proxy;
}
protected override object? Invoke(MethodInfo? method, object?[]? args)
{
Stopwatch watch = Stopwatch.StartNew();
object? result;
try
{
result = method!.Invoke(_target, args);
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
ExceptionDispatchInfo.Throw(ex.InnerException);
throw;
}
if (result is Task task)
{
// Süreyi Task tamamlandığında yaz; çağırana aynı Task'ı döndür
task.ContinueWith(t => Console.WriteLine($" [timing] {method.Name}: {watch.ElapsedMilliseconds} ms ({t.Status})"),
TaskContinuationOptions.ExecuteSynchronously);
return task;
}
Console.WriteLine($" [timing] {method.Name}: {watch.ElapsedMilliseconds} ms");
return result;
}
}
public static class DispatchProxyDemo
{
public static async Task RunAsync()
{
ICatalog naive = NaiveTimingProxy<ICatalog>.Create(new InMemoryCatalog());
Book? first = await naive.FindAsync("978-0441013593");
Console.WriteLine($"naive proxy -> {first?.Title}");
try
{
naive.FindCached("000");
}
catch (Exception ex)
{
Console.WriteLine($"naive proxy threw {ex.GetType().Name}");
}
ICatalog timed = TimingProxy<ICatalog>.Create(new InMemoryCatalog());
Book? second = await timed.FindAsync("978-0441013593");
Console.WriteLine($"fixed proxy -> {second?.Title}");
try
{
timed.FindCached("000");
}
catch (Exception ex)
{
Console.WriteLine($"fixed proxy threw {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine($"proxy type: {timed.GetType().Name}");
}
}
[timing] FindAsync: 1 ms
naive proxy -> Dune
naive proxy threw TargetInvocationException
[timing] FindAsync: 122 ms (RanToCompletion)
fixed proxy -> Dune
fixed proxy threw InvalidOperationException: Unknown ISBN 000
proxy type: generatedProxy_2
Naif proxy iki hata yapıyor ve ikisi de üretimde sessizce yanlış bilgi verir. Birincisi, 120 ms süren bir async method için 1 ms ölçtü. method.Invoke async bir method'u çağırdığında method ilk await'e kadar çalışıp bir Task döndürür; proxy o anda saati durdurursa yalnızca senkron kısmı ölçmüş olur. Düzeltme, sonuç bir Task ise süreyi task tamamlandığında yazmak ve çağırana aynı task'ı döndürmek. İkincisi, gerçek method'un fırlattığı InvalidOperationException, reflection tarafından bir TargetInvocationException içine sarılıp client'a ulaştı; catch (InvalidOperationException) yazan client onu yakalayamaz. Düzeltme, inner exception'ı ExceptionDispatchInfo.Throw ile orijinal stack trace'iyle yeniden fırlatmak.
DispatchProxy'nin sınırlarını da bilmek gerekiyor. Yalnızca interface'leri proxy'leyebilir, class'ları değil; ürettiği tip (generatedProxy_2) senin TimingProxy<T> class'ından türer. ValueTask döndüren method'lar için yukarıdaki Task kontrolü yetmez, ayrıca ele alınmalı. Kod runtime'da üretildiği için Native AOT (Ahead-of-Time) ile uyumlu değil; projeyi IsAotCompatible ile derlediğimde derleyici DispatchProxy.Create için IL3050 (AOT analyzer'ının uyarı kodu) uyarısı verdi: "Creating a proxy instance requires generating code at runtime." Class'ları da proxy'lemek ya da interceptor chain'i kurmak gerekiyorsa sektördeki standart araç Castle DynamicProxy (Castle.Core paketi): virtual member'ları override eden subclass'lar üretir. EF Core'un Microsoft.EntityFrameworkCore.Proxies paketi Castle.Core'a bağımlı; yazının başındaki Castle.Proxies.AuthorProxy adı oradan geliyor. Moq ve NSubstitute gibi mock kütüphaneleri de mock object'lerini aynı kütüphaneyle üretir.
Proxy mi Decorator mı?
Serinin Decorator yazısındaki cache ve log decorator'ları ile buradaki proxy'ler yapısal olarak neredeyse aynı: ikisi de aynı interface'i implement eder ve gerçek object'e bir referans tutar. GoF'un kendisi de bu benzerliği kabul eder ve farkı niyete bağlar: decorator bir object'e sorumluluk ekler, proxy ise o object'e erişimi kontrol eder.
| Soru | Decorator | Proxy |
|---|---|---|
| Niyet | Davranış eklemek (cache, log, retry) | Erişimi kontrol etmek (ertele, izin ver, uzağa taşı) |
| Gerçek object'i kim oluşturur? | Dışarıdan verilir, decorator yalnızca sarar | Çoğu zaman proxy oluşturur ya da yönetir (virtual, remote) |
| Üst üste dizilir mi? | Evet, birkaç decorator üst üste tipiktir | Genellikle tek katman |
| Client farkı bilir mi? | Hayır | Hayır, ama maliyeti (network, sorgu) hissedebilir |
Pratikte çizgi bulanık: TimingProxy<T>'yi davranış eklediği için decorator olarak da okuyabilirsin. Önemli olan isim değil, kodu okuyan kişinin niyeti anlaması; "bu katman erişimi mi kontrol ediyor, davranış mı ekliyor?" sorusunun cevabı adını da belirlesin.
Sık Düşülen Tuzaklar
Proxy'nin gizlediği maliyet. Lazy loading ve remote proxy, maliyetli bir işlemi bir property erişimine ya da sıradan bir method çağrısına benzetir. Yazının başındaki altı sorgu bir sayfa için önemsiz görünebilir; aynı döngü 500 yazarlık bir rapor endpoint'inde 501 sorgu demek. EF Core'da lazy loading'i açıyorsan, Entity Framework Core serisindeki "İlişkili Veri Yükleme: Include, Explicit ve Lazy Loading, Split Query" ve "Performans Temelleri: N+1, Projection, AsNoTracking ve Compiled Query" yazılarını oku ve SQL loglarını gerçekten izle.
Proxy'nin ömrü gerçek subject'in ömrüne bağlı. Context dispose edildikten sonra lazy-loading proxy'sine erişmek exception verir:
Author detached;
using (BookstoreContext db = new(connection))
detached = db.Authors.First();
try
{
Console.WriteLine(detached.Books.Count);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.LazyLoadOnDisposedContextWarning': An attempt was made to lazy-load navigation 'AuthorProxy.Books' after the associated DbContext was disposed or returned to the pool, or the entity was explicitly detached from the context. This exception can be suppressed or logged by passing event ID 'CoreEventId.LazyLoadOnDisposedContextWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.
Bir entity'yi controller'dan JSON'a (JavaScript Object Notation) serialize ederken de aynı mekanizma devreye girer: serializer her navigation'a dokunur ve ya ek sorgular tetiklenir ya da bu exception gelir. Entity yerine DTO (Data Transfer Object) döndürmek bu yüzden de önemli.
Proxy'yi çalıştıran kuralı unutmak. Lazy-loading proxy'leri yalnızca virtual navigation'larda ve sealed olmayan class'larda çalışır. Bir navigation'ı virtual yapmayı unutursan EF Core bunu model oluşturulurken yakalar:
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace Bookstore.Data.Broken;
public class Publisher
{
public int Id { get; set; }
public required string Name { get; set; }
// virtual unutulmuş
public ICollection<Imprint> Imprints { get; set; } = [];
}
public class Imprint
{
public int Id { get; set; }
public required string Name { get; set; }
public int PublisherId { get; set; }
}
public sealed class BrokenContext(SqliteConnection connection) : DbContext
{
public DbSet<Publisher> Publishers => Set<Publisher>();
protected override void OnConfiguring(DbContextOptionsBuilder options) =>
options.UseSqlite(connection).UseLazyLoadingProxies();
}
InvalidOperationException: Property 'Publisher.Imprints' is not virtual. 'UseChangeTrackingProxies' requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. 'UseLazyLoadingProxies' requires only the navigation properties be virtual.
Tip kimliğine güvenmek. Proxy'li bir entity için entity.GetType() == typeof(Author) false döner; tip adına göre çalışan loglama, switch ya da serialization kodu proxy'yi tanımaz. is Author kontrolü ise çalışır, çünkü proxy bir subclass.
Proxy içinde iş kuralı. Protection proxy'deki yetki kontrolü bir erişim kuralı; "iade tutarı sipariş toplamını geçemez" ise bir iş kuralı ve RefundService'in içinde durmalı. Proxy'yi kaldırdığında iş kuralı da kayboluyorsa yanlış yere yazılmıştır.
Ne Zaman Kullanma
Pahalı olmayan bir object için virtual proxy, ölçülmemiş bir optimizasyondur; constructor'ı birkaç mikrosaniye süren bir service'i Lazy<T> arkasına almak yalnızca okunabilirliği düşürür. Erişim kontrolünü framework zaten sağlıyorsa (ASP.NET Core authorization, veritabanı izinleri) aynı kuralı bir protection proxy'de tekrar etme; iki yerde yaşayan kural er geç ayrışır. Remote proxy'nin şeffaflığı ise network'ün varlığını unutturacak kadar ileri götürülmemeli: sık çağrılan, ince taneli bir yerel interface'i olduğu gibi uzağa taşımak yerine, toplu ve kaba taneli bir remote interface tasarla. Kesişen ihtiyaçlar (log, metrik, tracing) için DispatchProxy yazmadan önce de iki kez düşün: .NET'in ILogger'ı, System.Diagnostics.Metrics ve OpenTelemetry çoğu ihtiyacı reflection'sız ve AOT uyumlu biçimde karşılıyor.
Küçük Bir Deneme
★☆☆ CatalogClient'ın önüne, aynı ISBN'i (International Standard Book Number) bir istek boyunca yalnızca bir kez soran küçük bir cache katmanı koy. Bu katman proxy mi, decorator mı? Örnekteki üç HTTP isteği kaça iner?
★★☆ TimingProxy<T>'yi ValueTask ve ValueTask<T> döndüren method'lar için genişlet. Neden ValueTask'ı iki kez await etmemen gerektiğini ve bunun proxy tasarımını nasıl etkilediğini düşün.
★★★ Yazar sayfasındaki N+1'i lazy loading'i kapatmadan bul: LogTo ile bir istek boyunca çalışan sorguları say ve belirli bir eşiği aşınca uyarı loglayan bir mekanizma kur. Aynı mekanizmayı EF Core interceptor'larıyla nasıl yazardın?
Bir sonraki yazıyla behavioral pattern'lere geçiyoruz ve ilk durak, ASP.NET Core'da her istekte farkında olmadan kullandığın bir pattern: Chain of Responsibility. Middleware pipeline'ını, DelegatingHandler chain'ini ve Bookstore'da müdürden finans direktörüne uzanan bir indirim onay zincirini kuracağız.