Mediator Pattern: Object'ler Arası Konuşmayı Merkezileştirmek ve "MediatR'a Gerçekten İhtiyacım Var mı?"
Mediator, birbirini doğrudan çağıran bileşenlerin etkileşim kurallarını tek bir object'e toplar; MediatR ise bu pattern'den çok request tipine göre handler bulan bir in-process dispatcher'dır. Ödeme ekranında birbirini tanıyan bileşenlerin ürettiği hatayı bir CheckoutForm mediator'ıyla düzeltiyor, Blazor'un EditContext'ini gerçek bir örnek olarak inceliyor, ardından aynı sipariş işini MediatR 14.3, düz bir service class ve 30 satırlık kendi dispatcher'ımızla yazıp karşılaştırıyoruz.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un ödeme ekranında üç component var: kupon kutusu, kargo seçimi ve toplam tutar etiketi. Kurallar basit: FREESHIP kuponu kargoyu Standard'a çeker ve ücretsiz yapar; kullanıcı Express'e geçerse kupon geçersiz olur. Ekranı yazan geliştirici her component'e ihtiyaç duyduğu diğer component'lerin referansını vermiş:
public sealed class CouponBox
{
public ShippingSelector Shipping { get; set; } = null!;
public TotalLabel Total { get; set; } = null!;
public string? Code { get; private set; }
public void Apply(string code)
{
Code = code;
if (code == "FREESHIP")
Shipping.Select("Standard");
Total.Refresh();
}
public void Revalidate()
{
if (Code == "FREESHIP" && Shipping.Method != "Standard")
{
Code = null;
Console.WriteLine("Coupon removed");
Total.Refresh();
}
}
}
public sealed class ShippingSelector
{
public CouponBox Coupon { get; set; } = null!;
public TotalLabel Total { get; set; } = null!;
public string Method { get; private set; } = "Express";
public void Select(string method)
{
Method = method;
Total.Refresh();
Coupon.Revalidate();
}
}
public sealed class TotalLabel(decimal subtotal, CouponBox coupon, ShippingSelector shipping)
{
public void Refresh()
{
decimal fee = coupon.Code == "FREESHIP" ? 0m : shipping.Method == "Express" ? 25m : 10m;
Console.WriteLine($"Total: {subtotal + fee}");
}
}
DirectCheckout checkout = DirectCheckout.Create(subtotal: 80m);
Console.WriteLine("-- apply FREESHIP");
checkout.Coupon.Apply("FREESHIP");
Console.WriteLine("-- select Express");
checkout.Shipping.Select("Express");
Sepet 80, Standard kargo 10, Express 25. Kupon uygulandığında ekrana kaç kez "Total" yazılır? Kullanıcı ardından Express'i seçtiğinde etiket hangi tutarları, hangi sırayla gösterir? Toplam tutarı dinleyen bir ödeme component'i olsaydı hangi tutarla provizyon isterdi?
Birbirini Tanıyan Component'ler
Gerçek çıktı:
-- apply FREESHIP
Total: 80
Total: 80
-- select Express
Total: 80
Coupon removed
Total: 105
Kupon uygulanınca toplam iki kez hesaplandı: bir kez ShippingSelector.Select'in içinde, bir kez de CouponBox.Apply'ın sonunda. Express seçildiğinde daha kötüsü oldu: Select önce toplamı yeniledi ve kupon henüz düşmediği için etiket bir an 80 gösterdi; ancak ardından çağrılan Revalidate kuponu kaldırıp doğru tutarı, 105'i yazdı. Hiçbir class tek başına yanlış değil. Hata, "kupon kargo değişince yeniden kontrol edilir ve toplam en sonda hesaplanır" kuralının üç class'a dağılmış olmasında. Kuralın sırası, kimin kimi hangi sırayla çağırdığına bağlı.
Üç component arasında altı referans var; dördüncü bir component (ör. hediye paketi) eklendiğinde her biri diğer üçünü tanımak zorunda kalabilir. N component'li bir ekranda bağlantı sayısı N(N−1)'e doğru büyür. Bu yazının pattern'i bu büyümeyi kesmek için var.
GoF'un Tanımı: Etkileşimi Bir Object'e Toplamak
GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) Mediator'ı behavioral pattern'ler arasında sayar. Niyeti, bir grup object'in birbiriyle nasıl etkileşeceğini tek bir object'in içine almak; object'ler birbirine doğrudan referans vermediği için loose coupling sağlanır ve etkileşim kuralları onlardan bağımsız değiştirilebilir. Roller üç tane: Mediator (colleague'lerin haber verdiği interface), ConcreteMediator (koordinasyon mantığını bilen ve colleague'leri tanıyan class) ve Colleague (yalnız mediator'ını tanıyan component'ler). Kitabın motivasyon örneği de bizimkine çok yakın: bir font seçim diyalog kutusu. List box'ta seçim değişince entry field güncellenir, field boşsa button pasifleşir; bu kuralları widget'lar değil, bir DialogDirector bilir.
Kitap pattern'in bedelini de saklamaz: karmaşıklığı colleague'lerden alıp mediator'a taşırsın ve dikkat etmezsen mediator, kimsenin anlamadığı tek parça bir class'a dönüşür. GoF ayrıca Mediator'ı iki komşusundan ayırır. Facade bir alt sisteme tek yönlü, sade bir kapı açar; alt sistem facade'ı bilmez. Mediator'da ise iletişim çift yönlüdür, colleague'ler mediator'a haber verir. Facade'ın kendisi serinin "Facade Pattern: Karmaşık Alt Sistemi Tek Kapıdan Sunmak" yazısında. Colleague'lerin mediator'a haber vermesi için Observer da kullanılabilir; ona iki yazı sonra geleceğiz.
Bookstore'da Mediator: CheckoutForm
// Mediator
public interface ICheckoutMediator
{
void Changed(CheckoutComponent sender);
}
// Colleague: yalnız mediator'ı tanır
public abstract class CheckoutComponent(ICheckoutMediator mediator)
{
protected void Changed() => mediator.Changed(this);
}
public sealed class CouponBox(ICheckoutMediator mediator) : CheckoutComponent(mediator)
{
public string? Code { get; private set; }
public void Apply(string code)
{
Code = code;
Changed();
}
internal void Clear() => Code = null;
}
public sealed class ShippingSelector(ICheckoutMediator mediator) : CheckoutComponent(mediator)
{
public string Method { get; private set; } = "Express";
public void Select(string method)
{
Method = method;
Changed();
}
internal void Set(string method) => Method = method;
}
public sealed class TotalLabel
{
public void Show(decimal total) => Console.WriteLine($"Total: {total}");
}
// ConcreteMediator: etkileşim kuralları tek yerde
public sealed class CheckoutForm : ICheckoutMediator
{
private readonly decimal _subtotal;
public CheckoutForm(decimal subtotal)
{
_subtotal = subtotal;
Coupon = new CouponBox(this);
Shipping = new ShippingSelector(this);
}
public CouponBox Coupon { get; }
public ShippingSelector Shipping { get; }
public TotalLabel Total { get; } = new();
public void Changed(CheckoutComponent sender)
{
if (sender == Coupon && Coupon.Code == "FREESHIP")
Shipping.Set("Standard");
if (sender == Shipping && Coupon.Code == "FREESHIP" && Shipping.Method != "Standard")
{
Coupon.Clear();
Console.WriteLine("Coupon removed");
}
decimal fee = Coupon.Code == "FREESHIP" ? 0m : Shipping.Method == "Express" ? 25m : 10m;
Total.Show(_subtotal + fee);
}
}
Aynı senaryo:
-- apply FREESHIP
Total: 80
-- select Express
Coupon removed
Total: 105
Her kullanıcı aksiyonunda toplam bir kez ve doğru hesaplanıyor. Component'ler artık birbirinin adını bilmiyor; CouponBox'ın tek bildiği "bir şey değişti, mediator'a söyle". Kuralın sırası (önce kargoyu düzelt, sonra kuponu kontrol et, en son toplamı göster) tek bir method'da okunuyor. Mediator'ın colleague'leri değiştirirken yeniden Changed tetiklememesi için Set ve Clear gibi "sessiz" method'lar kullandık; aksi hâlde mediator kendi kendini çağırıp aynı döngüyü merkezde yeniden kurardı.
.NET'teki Gerçek Örnek: Blazor'un EditContext'i
ASP.NET Core'un Blazor form altyapısı bu pattern'in ders kitabı örneği. InputText gibi bir input component değeri değişince validator'ı ya da özet component'ini çağırmaz; EditContext.NotifyFieldChanged der. DataAnnotationsValidator EditContext'in OnFieldChanged event'ine, ValidationSummary de OnValidationStateChanged event'ine subscribe olur. Üçü birbirini tanımaz; hepsi aynı EditContext'i tanır. Bunu bir console uygulamasında, Blazor'un kendi tipleriyle canlandırabiliriz:
CheckoutModel model = new();
EditContext context = new(model);
// Validator: yalnız EditContext'i tanıyor
ValidationMessageStore messages = new(context);
context.OnFieldChanged += (_, e) =>
{
messages.Clear(e.FieldIdentifier);
if (e.FieldIdentifier.FieldName == nameof(CheckoutModel.Email) && !model.Email.Contains('@'))
messages.Add(e.FieldIdentifier, "Email is invalid");
context.NotifyValidationStateChanged();
};
// Summary: o da yalnız EditContext'i tanıyor
context.OnValidationStateChanged += (_, _) =>
Console.WriteLine($"Summary: [{string.Join("; ", context.GetValidationMessages())}]");
// Input: değeri yazıp EditContext'e haber veriyor
model.Email = "ada";
context.NotifyFieldChanged(context.Field(nameof(CheckoutModel.Email)));
model.Email = "ada@example.com";
context.NotifyFieldChanged(context.Field(nameof(CheckoutModel.Email)));
Console.WriteLine($"Modified: {context.IsModified()}");
Summary: [Email is invalid]
Summary: []
Modified: True
Projeye yalnız <FrameworkReference Include="Microsoft.AspNetCore.App" /> ekledik; tipler ASP.NET Core'un shared framework'ünde. EditContext burada ConcreteMediator, input, validator ve summary de colleague'ler. Formuna yeni bir validator eklemek için input component'ine dokunman gerekmiyor, çünkü input kimin dinlediğini hiç bilmiyor. Dikkat edersen mediator'ın colleague'lere haber vermesi için event, yani Observer kullanılıyor; GoF'un iki pattern arasındaki ilişki için söylediği tam olarak bu.
MediatR Aslında Ne?
.NET dünyasında "mediator" denince çoğu kişinin aklına MediatR library'si gelir. Aynı sipariş verme işini MediatR ile yazalım:
public sealed record PlaceOrder(int CustomerId, IReadOnlyList<string> Isbns) : IRequest<OrderPlaced>;
public sealed class PlaceOrderHandler(Inventory inventory) : IRequestHandler<PlaceOrder, OrderPlaced>
{
public Task<OrderPlaced> Handle(PlaceOrder request, CancellationToken cancellationToken) =>
Task.FromResult(inventory.Reserve(request.Isbns));
}
public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
Console.WriteLine($"-> {typeof(TRequest).Name}");
TResponse response = await next(cancellationToken);
Console.WriteLine($"<- {typeof(TRequest).Name}");
return response;
}
}
ServiceCollection services = new();
services.AddLogging(b => b.AddSimpleConsole(o => o.SingleLine = true));
services.AddSingleton<Bookstore.Orders.Inventory>();
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Bookstore.Orders.WithMediatR.PlaceOrderHandler>();
cfg.AddOpenBehavior(typeof(Bookstore.Orders.WithMediatR.LoggingBehavior<,>));
});
using ServiceProvider provider = services.BuildServiceProvider();
MediatR.IMediator mediator = provider.GetRequiredService<MediatR.IMediator>();
Bookstore.Orders.OrderPlaced result = await mediator.Send(
new Bookstore.Orders.WithMediatR.PlaceOrder(CustomerId: 7, Isbns: ["978-0441013593", "978-0201633610"]));
Console.WriteLine(result);
-> PlaceOrder
warn: LuckyPennySoftware.MediatR.License[0] You do not have a valid license key for the Lucky Penny software MediatR. This is allowed for development and testing scenarios. If you are running in production you are required to have a licensed version. Please visit https://luckypennysoftware.com to obtain a valid license.
<- PlaceOrder
OrderPlaced { OrderId = 1001, Total = 73 }
MediatR'ın kendi tanımı "in-process messaging": Send'e verdiğin request'in tipine bakar, DI (dependency injection) container'dan o tip için kayıtlı tek handler'ı bulur, araya kayıtlı pipeline behavior'ları sarar ve çağırır. Publish ise bir notification'ı o tip için kayıtlı bütün handler'lara dağıtır. Bunun GoF Mediator'la ortak noktası, gönderenin alıcıyı tanımaması. Farkı ise daha büyük: GoF'un ConcreteMediator'ı colleague'leri tanır ve aralarındaki etkileşim kuralını içerir; CheckoutForm kuponla kargo arasındaki ilişkiyi bilir. MediatR'ın içinde hiçbir iş kuralı yoktur, yalnız "bu tip request'i şu handler'a götür" mapping'i vardır. Request'ler birbiriyle konuşmaz; çoğu zaman tek bir request'in tek bir handler'ı vardır. GoF diliyle bu, request'i bir object'e çeviren Command ile handler'ı tipten çözen bir dispatcher'ın birleşimi; Command serinin "Command Pattern: İsteği Object'e Çevirmek, Undo/Redo ve Kuyruğa Atmak" yazısında. Pipeline behavior'lar da handler'ın etrafını saran bir Chain of Responsibility; ASP.NET Core middleware'iyle aynı fikir, serinin "Chain of Responsibility: ASP.NET Core Middleware, DelegatingHandler ve Validation Zinciri" yazısında.
Çıktıdaki warn satırı da gerçek. Kullandığımız sürüm MediatR 14.3.0; 13.0 (Temmuz 2025) ve sonrası Lucky Penny Software'in ticari lisansı ile RPL (Reciprocal Public License) 1.5 arasında dual license ile dağıtılıyor. Lisans anahtarı yoksa çalışma engellenmiyor, yalnız bu uyarı log'lanıyor; yıllık geliri 5 milyon doların altındaki şirketler için (dış yatırım ve kamu kurumu gibi ek koşullarla) ücretsiz bir Community lisansı var ama o da anahtar istiyor. Bu bilgiler zamana duyarlı (bu yazı Kasım 2026'da mediatr.io ve NuGet sayfasıyla kontrol edildi). Kütüphanelerin karşılaştırması ve lisansın proje kararına etkisi ilerideki Vertical Slice Architecture serisinin üçüncü yazısında; burada yalnız bir soruya bakıyoruz: kütüphanesiz de olur mu?
Kütüphanesiz (1): Düz Bir Service Class
MediatR'ın sağladığı ayrıştırmanın büyük kısmı, handler'ı ayrı bir class yapmaktan geliyor; dispatcher'dan değil. Aynı handler'ı interface'siz, library'siz yazıp doğrudan inject edebilirsin:
public sealed record PlaceOrder(int CustomerId, IReadOnlyList<string> Isbns);
// Ne interface ne library: endpoint bu class'ı doğrudan inject eder
public sealed class PlaceOrderHandler(Inventory inventory)
{
public Task<OrderPlaced> Handle(PlaceOrder request, CancellationToken ct = default) =>
Task.FromResult(inventory.Reserve(request.Isbns));
}
public static class OrderEndpoints
{
public static void Configure(WebApplicationBuilder builder) =>
builder.Services.AddScoped<PlaceOrderHandler>();
public static void Map(WebApplication app) =>
app.MapPost("/orders", async (PlaceOrder command, PlaceOrderHandler handler, CancellationToken ct) =>
TypedResults.Ok(await handler.Handle(command, ct)));
}
OrderPlaced { OrderId = 1001, Total = 73 }
Endpoint handler'ı tanıyor, evet; ama zaten IMediator'ı ve request tipini tanıyordu. Kazandığın şey somut: handler.Handle üzerinde go-to-definition doğrudan iş koduna gidiyor, derleyici eksik handler'ı çalışma zamanında değil derlemede yakalıyor, call stack'te araya girmiş generic wrapper'lar yok. Logging, validation ya da transaction gibi cross-cutting işler için pipeline behavior yerine ASP.NET Core'un endpoint filter'larını ya da serinin "Decorator Pattern: Caching, Logging ve Retry'ı Koda Dokunmadan Eklemek" yazısındaki decorator'ları kullanırsın. Filter'ların kendisi ASP.NET Core Web API (Application Programming Interface) serisindeki "ASP.NET Core Web API'de Filter'lar: Action Filter, Result Filter ve ServiceFilter" yazısında.
Kütüphanesiz (2): 30 Satırlık Kendi Dispatcher'ın
Ekip yine de "her request tek bir kapıdan geçsin ve behavior'lar her request'e otomatik uygulansın" diyorsa, bunun için gereken kod sanıldığından küçük:
public interface IRequest<TResponse>;
public interface IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken ct);
}
public interface IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, Func<Task<TResponse>> next, CancellationToken ct);
}
public sealed class Dispatcher(IServiceProvider services)
{
private static readonly ConcurrentDictionary<Type, object> Pipelines = new();
public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken ct = default)
{
// Request'in gerçek tipi ile kapalı generic Pipeline'ı bir kez kurup cache'liyoruz
Pipeline<TResponse> pipeline = (Pipeline<TResponse>)Pipelines.GetOrAdd(request.GetType(),
t => Activator.CreateInstance(typeof(Pipeline<,>).MakeGenericType(t, typeof(TResponse)))!);
return pipeline.Run(request, services, ct);
}
private abstract class Pipeline<TResponse>
{
public abstract Task<TResponse> Run(object request, IServiceProvider sp, CancellationToken ct);
}
private sealed class Pipeline<TRequest, TResponse> : Pipeline<TResponse> where TRequest : IRequest<TResponse>
{
public override Task<TResponse> Run(object request, IServiceProvider sp, CancellationToken ct)
{
TRequest typed = (TRequest)request;
IRequestHandler<TRequest, TResponse> handler = sp.GetRequiredService<IRequestHandler<TRequest, TResponse>>();
Func<Task<TResponse>> next = () => handler.Handle(typed, ct);
foreach (IPipelineBehavior<TRequest, TResponse> behavior in sp.GetServices<IPipelineBehavior<TRequest, TResponse>>().Reverse())
{
Func<Task<TResponse>> inner = next;
next = () => behavior.Handle(typed, inner, ct);
}
return next();
}
}
}
Dispatcher class'ı yorum ve boş satırlar hariç 29 satır. Tek zor nokta, Send(request) çağrısında derleyicinin yalnız TResponse'u bilmesi: request'in gerçek tipi çalışma zamanında belli oluyor. O yüzden MakeGenericType ile her request tipi için bir kez kapalı generic bir Pipeline<PlaceOrder, OrderPlaced> üretip cache'liyoruz; sonraki çağrılarda reflection yok. Behavior'lar registration sırasıyla dıştan içe sarılıyor. Registration ve kullanım:
ServiceCollection services = new();
services.AddSingleton<Bookstore.Orders.Inventory>();
services.AddSingleton<Bookstore.Messaging.Dispatcher>();
services.AddScoped<Bookstore.Messaging.IRequestHandler<Bookstore.Orders.OwnDispatcher.PlaceOrder, Bookstore.Orders.OrderPlaced>,
Bookstore.Orders.OwnDispatcher.PlaceOrderHandler>();
services.AddScoped(typeof(Bookstore.Messaging.IPipelineBehavior<,>), typeof(Bookstore.Orders.OwnDispatcher.LoggingBehavior<,>));
using ServiceProvider provider = services.BuildServiceProvider();
Bookstore.Messaging.Dispatcher dispatcher = provider.GetRequiredService<Bookstore.Messaging.Dispatcher>();
Bookstore.Orders.OrderPlaced result = await dispatcher.Send(
new Bookstore.Orders.OwnDispatcher.PlaceOrder(CustomerId: 7, Isbns: ["978-0441013593", "978-0201633610"]));
Console.WriteLine(result);
-> PlaceOrder
<- PlaceOrder
OrderPlaced { OrderId = 1001, Total = 73 }
Çıktı MediatR'ınkiyle aynı, uyarı satırı hariç. Neyin eksik olduğunu da dürüstçe söyleyelim: notification'lar ve Publish, stream request'ler, assembly tarama ile otomatik registration, exception handler'lar ve generic variance ile bir handler'ın birden fazla request tipine uyması yok. Bir örnekte dikkat edilecek bir ayrıntı daha var: Dispatcher'ı singleton kaydettik ve root IServiceProvider'dan handler çözüyor; scoped handler'ları doğru scope'tan almak için ASP.NET Core'da dispatcher'ı scoped kaydetmen gerekir. Bu tuzak C# İleri serisindeki "C#'ta Dependency Injection Temelleri: Lifetime'lar, Container ve Captive Dependency" yazısında anlatılıyor.
Sık Düşülen Tuzaklar
Navigasyonun kaybolması. await mediator.Send(new PlaceOrder(...)) satırında go-to-definition seni handler'a değil, ISender.Send'in tanımına götürür. Handler'ı bulmak için request tipinin "find all references" listesinde dolaşırsın. Küçük bir kod tabanında önemsiz görünen bu bedel, yüzlerce request'li bir projede yeni gelen birinin kodu okuma hızını ciddi biçimde düşürür.
Handler'dan handler'a Send. Bir handler'ın içinden başka bir request göndermek, call graph'ı tamamen gizler: PlaceOrderHandler stok, ödeme ve e-posta request'lerini mediator üzerinden çağırıyorsa hangi işin hangi transaction'da çalıştığını kodu okuyarak çıkaramazsın. Ortak bir iş varsa onu düz bir service class'a al ve iki handler'a inject et.
"Her şey mediator'dan geçsin" kuralı. Tek satırlık bir okuma sorgusu için request, handler ve registration üçlüsü yazmak, Mediator'ın çözdüğü bir problem olmadan yalnız dolaylılık ekler. Kuralı ekip tartışmadan benimsemiş olabilir; ilk iş "bu dolaylılık bize ne kazandırıyor?" diye sormak.
GoF Mediator'ın god object'e dönüşmesi. CheckoutForm bugün iki kuralı biliyor. Hediye paketi, sadakat puanı ve taksit seçimi eklendiğinde Changed method'u kimsenin dokunmaya cesaret edemediği bir if yığınına dönüşebilir. GoF'un uyardığı monolith tam olarak bu. Belirti gördüğünde kuralları adı olan küçük method'lara, gerekirse birden fazla mediator'a böl.
Notification'larda sıra ve exception. MediatR'ın varsayılan notification publisher'ı handler'ları sırayla await eder; biri exception fırlatırsa sonrakiler çalışmaz. Bir notification'a "olsa iyi olur" türünden işler (e-posta, cache temizliği) bağlıyorsan bu davranışı bilerek seç. Aynı problem Observer'da da var ve orada ayrıntılı ölçeceğiz.
Ne Zaman Kullanma
GoF Mediator için: iki component arasındaki tek bir ilişki için mediator kurma; doğrudan bir çağrı ya da bir event yeter. Pattern, birbirine bağlı üç ve daha fazla component'in etkileşim kuralları sıraya bağlı hâle geldiğinde, yani açılıştaki gibi "kim kimi hangi sırayla çağırıyor" sorusu hata ürettiğinde kendini öder.
MediatR ya da kendi dispatcher'ın için: request'lerin büyük çoğunluğu tek bir endpoint'ten çağrılıyor ve cross-cutting işleri zaten framework'ün filter'ları hallediyorsa, dispatcher bir şey kazandırmaz; düz service class'la başla. Dispatcher'ı gerçekten değerli yapan durumlar şunlar: aynı request'i HTTP (Hypertext Transfer Protocol) endpoint'i, background job ve message consumer gibi birden fazla giriş noktasından çağırmak ve hepsine aynı behavior zincirini uygulamak. O noktaya geldiğinde 30 satırlık dispatcher mı, MediatR mı, başka bir library mi sorusu bir mimari karar olur; o kararın tamamı ilerideki Vertical Slice Architecture serisinde.
Küçük Bir Deneme
★☆☆ CheckoutForm'a bir GiftWrap colleague'i ekle: seçilirse toplama 5 eklensin, FREESHIP ile birlikte seçilemesin. CouponBox ve ShippingSelector'da kaç satır değişti?
★★☆ Kendi dispatcher'ına ValidationBehavior ekle: request'te boş Isbns listesi varsa handler'a hiç gitmeden exception fırlatsın. Behavior'ları farklı sırayla register edip çıktının sırasının nasıl değiştiğine bak.
★★★ Dispatcher'a Publish<TNotification> ekle ve iki farklı strateji yaz: handler'ları sırayla çağıran ve ilk exception'da duran, bir de hepsini çalıştırıp exception'ları bir AggregateException'da toplayan. İkisini birer handler'ı exception fırlatacak şekilde dene ve hangi işler için hangisini seçeceğine karar ver.
Mediator object'ler arasındaki konuşmayı merkeze aldı. Bir sonraki yazı bir object'in kendi geçmişiyle ilgileniyor: çok adımlı ödeme formunun taslağını snapshot olarak almak, "geri al" demek ve bunu yaparken object'in encapsulation'ını bozmamak, yani Memento.