Chain of Responsibility: ASP.NET Core Middleware, DelegatingHandler ve Validation Zinciri
Chain of Responsibility, isteği gönderenle ele alan arasındaki coupling'i kırmak için isteği bir handler chain'i boyunca, biri ele alana kadar iletir; ASP.NET Core middleware'i ve DelegatingHandler bu pattern'in short-circuit edebilen biçimleri. Bookstore'da indirim onay chain'i, DI'da IEnumerable kayıt sırasının taşıdığı gizli iş kuralı, ilk hatada duran validation chain'i ile her şeyi toplayan pipeline farkı ve chain'in sonundan düşen istek tuzağı.
Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un katalog API'sinde (Application Programming Interface) iki middleware ve bir endpoint var: biri her isteğin süresini ölçüyor, diğeri X-Api-Key header'ı olmayan istekleri reddediyor. Uygulamayı gerçek bir port açmadan, Microsoft.AspNetCore.TestHost'un TestServer'ı ile çalıştırıyoruz; önce header'sız, sonra header'lı iki istek gönderiyoruz.
using Microsoft.AspNetCore.TestHost;
namespace Bookstore.Demos;
public static class MiddlewareDemo
{
public static async Task RunAsync()
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Logging.ClearProviders();
await using WebApplication app = builder.Build();
// 1. middleware: her isteği görür, cevaptan sonra da çalışır
app.Use(async (context, next) =>
{
Console.WriteLine($"-> timing {context.Request.Path}");
await next(context);
Console.WriteLine($"<- timing {context.Response.StatusCode}");
});
// 2. middleware: API key yoksa isteği burada bitirir, next çağrılmaz
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Api-Key"))
{
Console.WriteLine(" api-key: missing, short-circuit");
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
await next(context);
});
app.MapGet("/books/{isbn}", (string isbn) =>
{
Console.WriteLine($" endpoint: book {isbn}");
return Results.Ok(new { isbn, title = "Dune" });
});
await app.StartAsync();
HttpClient client = app.GetTestClient();
await client.GetAsync("/books/978-0441013593");
Console.WriteLine();
client.DefaultRequestHeaders.Add("X-Api-Key", "demo");
await client.GetAsync("/books/978-0441013593");
}
}
İlk istekte hangi satırlar, hangi sırayla yazılır? "<- timing" satırı ilk istekte de görünür mü, görünürse hangi status code ile?
İsteği Ele Alan Durur, Diğerleri İletir
-> timing /books/978-0441013593
api-key: missing, short-circuit
<- timing 401
-> timing /books/978-0441013593
endpoint: book 978-0441013593
<- timing 200
İlk istekte endpoint hiç çalışmadı. API key middleware'i isteği ele aldı, 401 yazdı ve next'i çağırmadı; ASP.NET Core dokümantasyonunun deyişiyle isteği short-circuit etti. Timing middleware'i ise çağrının geri dönüşünde yine çalıştı ve 401'i gördü. İkinci istekte API key middleware'i "bu benim işim değil" deyip isteği bir sonrakine iletti, istek endpoint'e kadar ulaştı.
Bu davranışın bir adı var: Chain of Responsibility. ASP.NET Core'un request pipeline'ı bu pattern'in .NET'teki en bilinen örneği. Bu yazıda pattern'in GoF'taki tanımını, Bookstore'da müdürden finans direktörüne uzanan bir indirim onay chain'ini, aynı chain'i DI (Dependency Injection) ile kurarken kayıt sırasının taşıdığı gizli anlamı, HttpClient'ın DelegatingHandler chain'ini ve ilk hatada duran bir validation chain'ini konuşacağız. Middleware'in kendisi için ASP.NET Core Web API serisindeki "Middleware Pipeline: Sıralama, Use, Run, Map ve Kendi Middleware'in" yazısına bakabilirsin; burada pattern tarafına odaklanıyoruz.
GoF'un Tanımı: İsteği Ele Alana Kadar İlerletmek
Chain of Responsibility, 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 behavioral pattern'lerinden biri. Amacı, kendi cümlelerimle: bir isteği gönderen ile onu ele alacak object arasındaki coupling'i kırmak için birden fazla object'e isteği ele alma şansı vermek; bu object'leri bir chain hâline getirip istek biri tarafından ele alınana kadar chain boyunca iletmek.
Roller iki tane. Handler, isteği ele almak için bir interface tanımlar ve isteğe bağlı olarak bir sonraki handler'a (GoF'un deyişiyle successor) referans tutar. ConcreteHandler, ele alabildiği isteği ele alır, alamadığını successor'a iletir. Client isteği chain'in başına verir ve kimin ele alacağını bilmez. Kitabın örneği bir kullanıcı arayüzündeki bağlama duyarlı yardım: kullanıcı bir düğmede yardım istediğinde istek önce düğmeye, düğme cevap veremezse onu içeren diyaloğa, o da veremezse uygulamaya gider.
GoF'un "consequences" bölümünde saydığı üç sonuç bu yazının da iskeleti. Coupling azalır: gönderen, alıcının kim olduğunu bilmez. Esneklik artar: chain'e handler eklemek, çıkarmak ya da sırasını değiştirmek client'ı etkilemez. Ve en çok unutulan: isteğin ele alınması garanti değildir. Hiçbir handler sorumluluk almazsa istek chain'in sonundan düşer.
Bir ayrım daha yapmak gerekiyor. GoF'un tarif ettiği klasik biçimde handler'lardan yalnızca biri isteği ele alır, diğerleri sadece iletir. ASP.NET Core middleware'i bunun genişletilmiş bir biçimi: her middleware isteği hem ilerletmeden önce hem de cevap döndükten sonra işleyebilir ve çoğu zaman hepsi bir şey yapar. Bu ikinci biçim Pipeline ya da Pipes and Filters olarak da anılır; serinin ilerideki "GoF Dışında Bilmen Gereken Kod Seviyesi Pattern'ler" yazısında ayrıca ele alacağız. İkisini ayıran soru basit: handler'lardan biri isteği ele aldığında chain duruyor mu?
Kendi Chain'in: İndirim Onayı
Bookstore'un kurumsal satış ekibi büyük siparişlerde indirim veriyor. Yüzde 5'e kadar ekip lideri, yüzde 20'ye kadar satış direktörü onaylayabiliyor; finans direktörü yüzde 40'a kadar çıkabiliyor ama yalnızca 1000 EUR ve üstü siparişlerde. Bunun üstü kimsenin yetkisinde değil.
namespace Bookstore.Discounts;
public sealed record DiscountRequest(string Customer, decimal OrderTotal, decimal Percent);
// Handler: isteği ya kendisi ele alır ya da chain'de bir sonrakine iletir
public abstract class Approver
{
private Approver? _next;
protected abstract string Title { get; }
protected abstract bool CanApprove(DiscountRequest request);
public Approver SetNext(Approver next)
{
_next = next;
return next;
}
public string Handle(DiscountRequest request)
{
if (CanApprove(request))
return $"approved by {Title}";
return _next is not null
? _next.Handle(request)
: $"rejected: nobody can approve {request.Percent}% on {request.OrderTotal} EUR";
}
}
public sealed class TeamLead : Approver
{
protected override string Title => "team lead";
protected override bool CanApprove(DiscountRequest request) => request.Percent <= 5;
}
public sealed class SalesDirector : Approver
{
protected override string Title => "sales director";
protected override bool CanApprove(DiscountRequest request) => request.Percent <= 20;
}
// Finans direktörü yüksek indirimi yalnızca kurumsal büyüklükteki siparişte verir
public sealed class FinanceDirector : Approver
{
protected override string Title => "finance director";
protected override bool CanApprove(DiscountRequest request) => request.Percent <= 40 && request.OrderTotal >= 1000;
}
public static class ClassicChainDemo
{
public static void Run()
{
Approver chain = new TeamLead();
chain.SetNext(new SalesDirector()).SetNext(new FinanceDirector());
DiscountRequest[] requests =
[
new("Ada", 300m, 5m),
new("Alan", 300m, 15m),
new("Grace", 2400m, 35m),
new("Linus", 300m, 35m),
new("Barbara", 5000m, 60m)
];
foreach (DiscountRequest request in requests)
Console.WriteLine($"{request.Customer,-8} {request.Percent,3}% of {request.OrderTotal,5} EUR -> {chain.Handle(request)}");
}
}
Ada 5% of 300 EUR -> approved by team lead
Alan 15% of 300 EUR -> approved by sales director
Grace 35% of 2400 EUR -> approved by finance director
Linus 35% of 300 EUR -> rejected: nobody can approve 35% on 300 EUR
Barbara 60% of 5000 EUR -> rejected: nobody can approve 60% on 5000 EUR
Satış ekranı yalnızca chain.Handle(request) çağırıyor; onay hiyerarşisini, limitleri ve kimin hangi koşulda devreye girdiğini bilmiyor. Yarın "yüzde 10'a kadar bölge müdürü" diye yeni bir kademe geldiğinde yeni bir Approver class'ı yazılıp chain'e bir satırla eklenecek; ekranın koduna dokunulmayacak. Son iki satıra da dikkat et: 300 EUR'luk siparişte yüzde 35'i finans direktörü dahil kimse onaylayamıyor ve istek chain'in sonundan düşüyor. Bu sonucu bilinçli olarak üreten şey Handle'daki son dal; o dal olmasaydı kod null döndürür ya da hiçbir şey yapmadan geçerdi. Chain'in sonunda ne olacağı, chain'in tasarımının parçası olmalı.
Bir de GoF'un önerdiği yapıyla aradaki küçük farka bak: SetNext eklenen handler'ı döndürüyor, böylece chain tek bir fluent ifadeyle kurulabiliyor. Yine de bu klasik biçimin bir zayıflığı var: chain'in yapısı, kodu okuyan kişinin SetNext çağrılarını takip etmesini gerektiriyor ve her handler bir sonrakine referans tuttuğu için handler'lar test edilirken de bu bağlantıyı kurmak gerekiyor.
Chain'i DI ile Kurmak: IEnumerable<T> ve Kayıt Sırası
.NET'te daha yaygın olan biçim, handler'ların birbirini hiç tanımadığı bir versiyon: her approver aynı interface'i implement ediyor, DI container'a aynı servis tipiyle kaydediliyor ve chain bunları IEnumerable<IDiscountApprover> olarak alıp ilk "evet" diyeni seçiyor. Successor referansı ortadan kalkıyor, sıra container'a taşınıyor.
namespace Bookstore.Discounts.Di;
public interface IDiscountApprover
{
string Title { get; }
bool CanApprove(DiscountRequest request);
}
public sealed class TeamLeadApprover : IDiscountApprover
{
public string Title => "team lead";
public bool CanApprove(DiscountRequest request) => request.Percent <= 5;
}
public sealed class SalesDirectorApprover : IDiscountApprover
{
public string Title => "sales director";
public bool CanApprove(DiscountRequest request) => request.Percent <= 20;
}
public sealed class FinanceDirectorApprover : IDiscountApprover
{
public string Title => "finance director";
public bool CanApprove(DiscountRequest request) => request.Percent <= 40 && request.OrderTotal >= 1000;
}
// Chain'in sırası, DI'a kayıt sırasıdır
public sealed class DiscountApprovalChain(IEnumerable<IDiscountApprover> approvers)
{
public string Handle(DiscountRequest request) =>
approvers.FirstOrDefault(approver => approver.CanApprove(request)) is { } approver
? $"approved by {approver.Title}"
: $"rejected: nobody can approve {request.Percent}% on {request.OrderTotal} EUR";
}
public static class DiChainDemo
{
public static void Run()
{
ServiceCollection services = new();
services.AddSingleton<IDiscountApprover, TeamLeadApprover>();
services.AddSingleton<IDiscountApprover, SalesDirectorApprover>();
services.AddSingleton<IDiscountApprover, FinanceDirectorApprover>();
services.AddSingleton<DiscountApprovalChain>();
Print("registered team lead first", services);
// Biri finans direktörünü "en önemli" diye en başa kaydetti
ServiceCollection reordered = new();
reordered.AddSingleton<IDiscountApprover, FinanceDirectorApprover>();
reordered.AddSingleton<IDiscountApprover, TeamLeadApprover>();
reordered.AddSingleton<IDiscountApprover, SalesDirectorApprover>();
reordered.AddSingleton<DiscountApprovalChain>();
Print("registered finance director first", reordered);
}
private static void Print(string title, ServiceCollection services)
{
using ServiceProvider provider = services.BuildServiceProvider();
DiscountApprovalChain chain = provider.GetRequiredService<DiscountApprovalChain>();
Console.WriteLine(title);
foreach (DiscountRequest request in (DiscountRequest[])[new("Grace", 2400m, 5m), new("Grace", 2400m, 15m)])
Console.WriteLine($" {request.Percent,3}% of {request.OrderTotal,5} EUR -> {chain.Handle(request)}");
}
}
registered team lead first
5% of 2400 EUR -> approved by team lead
15% of 2400 EUR -> approved by sales director
registered finance director first
5% of 2400 EUR -> approved by finance director
15% of 2400 EUR -> approved by finance director
Kodun hiçbir satırı değişmedi, yalnızca Program.cs'teki kayıt sırası değişti ve 2400 EUR'luk bir siparişteki yüzde 5'lik indirim artık finans direktörünün onay listesine düşüyor. Derleyici bir şey söylemedi, testler (eğer her approver ayrı ayrı test ediliyorsa) yeşil. Microsoft.Extensions.DependencyInjection, aynı servis tipiyle kaydedilen implementation'ları IEnumerable<T> olarak kayıt sırasıyla verir; bu bir kaza değil, belgelenmiş bir davranış. Ama o sıranın bir iş kuralı taşıdığını kodda hiçbir şey söylemiyor.
Bunun iki çaresi var. Birincisi sırayı açık hâle getirmek: interface'e bir int Order property'si ekleyip chain'in constructor'ında OrderBy ile sıralamak; artık sıra, kayıt satırlarının dizilişine değil approver'ın kendisine ait. İkincisi, sırayı önemsiz kılmak: her approver'ın koşulu birbirini dışlayacak şekilde yazılırsa (ekip lideri 0–5, direktör 5–20, finans 20–40), hangisinin önce sorulduğu sonucu değiştirmez. Hangisini seçersen seç, chain'in tamamını bir test ile doğrula: "yüzde 5, 2400 EUR, ekip lideri onaylar" testi, yukarıdaki yeniden sıralamayı anında yakalardı. DI'ın lifetime ve kayıt mekaniği için C# İleri serisindeki "Dependency Injection Temelleri" yazısına bakabilirsin.
DelegatingHandler: HttpClient'ın Giden İstek Chain'i
Middleware gelen isteklerin chain'iyse, DelegatingHandler giden isteklerin chain'i. Her handler InnerHandler adında bir sonraki handler'a referans tutuyor (GoF'un successor'ı) ve base.SendAsync ile isteği ona iletiyor; en sonda gerçek network işini yapan primary handler (varsayılan olarak SocketsHttpHandler) duruyor. IHttpClientFactory ile chain'i kurarken sıra yine kayıt sırası: ilk eklenen handler en dışta çalışır.
using System.Net;
namespace Bookstore.Demos;
// İsteğe correlation id ekler, sonra chain'de ilerletir
public sealed class CorrelationIdHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Add("X-Correlation-Id", "c-42");
Console.WriteLine("-> correlation");
HttpResponseMessage response = await base.SendAsync(request, ct);
Console.WriteLine("<- correlation");
return response;
}
}
// Aynı GET'i ikinci kez görürse network'e hiç gitmeden cevaplar
public sealed class CachingHandler : DelegatingHandler
{
private readonly Dictionary<string, string> _cache = [];
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
string key = request.RequestUri!.ToString();
if (_cache.TryGetValue(key, out string? body))
{
Console.WriteLine(" cache: hit, short-circuit");
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(body) };
}
Console.WriteLine(" cache: miss");
HttpResponseMessage response = await base.SendAsync(request, ct);
_cache[key] = await response.Content.ReadAsStringAsync(ct);
return response;
}
}
// Chain'in sonu: gerçek uygulamada SocketsHttpHandler, burada network'e çıkmayan bir stub
public sealed class CatalogStubHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
Console.WriteLine($" network: {request.RequestUri!.AbsolutePath} (correlation {request.Headers.GetValues("X-Correlation-Id").Single()})");
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"title":"Dune"}""") });
}
}
public static class HandlerDemo
{
public static async Task RunAsync()
{
ServiceCollection services = new();
services.AddTransient<CorrelationIdHandler>();
services.AddTransient<CachingHandler>();
// İlk eklenen handler en dışta çalışır
services.AddHttpClient("catalog", client => client.BaseAddress = new Uri("https://catalog.example/"))
.AddHttpMessageHandler<CorrelationIdHandler>()
.AddHttpMessageHandler<CachingHandler>()
.ConfigurePrimaryHttpMessageHandler(() => new CatalogStubHandler());
using ServiceProvider provider = services.BuildServiceProvider();
HttpClient client = provider.GetRequiredService<IHttpClientFactory>().CreateClient("catalog");
await client.GetStringAsync("books/978-0441013593");
Console.WriteLine();
await client.GetStringAsync("books/978-0441013593");
}
}
-> correlation
cache: miss
network: /books/978-0441013593 (correlation c-42)
<- correlation
-> correlation
cache: hit, short-circuit
<- correlation
İkinci istekte cache handler'ı isteği ele aldı ve chain'in geri kalanına iletmedi; network'e hiç gidilmedi. Correlation handler'ı ise iki istekte de hem gidişte hem dönüşte çalıştı. Middleware'deki API key kontrolünün birebir aynısı, yalnızca yön ters. Microsoft'un HTTP (Hypertext Transfer Protocol) isteklerini anlatan dokümantasyonu da bu chain'i "outgoing request middleware" (giden istek middleware'i) diye adlandırıyor. Bu serinin Decorator yazısında aynı DelegatingHandler'ı decorator olarak görmüştük; ikisi birden doğru. Tek bir handler'a bakınca o bir decorator: aynı abstraction'ı sarıp davranış ekliyor. Chain'in bütününe ve özellikle short-circuit eden handler'a bakınca Chain of Responsibility.
Validation Chain'i: İlk Hatada Durmak mı, Hepsini Toplamak mı?
Chain of Responsibility'yi kurmak için her zaman class hiyerarşisi gerekmez. C#'ta bir handler, "ya cevap ver ya da null ile geç" diyen bir Func olabilir; chain de o fonksiyonların bir array'i:
namespace Bookstore.Orders;
public sealed record OrderLine(string Isbn, int Quantity);
public sealed record OrderDraft(string Customer, string ShippingCountry, IReadOnlyList<OrderLine> Lines);
public static class OrderRules
{
// Her kural ya bir hata döndürür ya da null ile "bende sorun yok, sıradakine geç" der
private static readonly Func<OrderDraft, string?>[] Rules =
[
order => order.Lines.Count == 0 ? "order has no lines" : null,
order => order.Lines.Any(line => line.Quantity <= 0) ? "quantities must be positive" : null,
order => order.ShippingCountry is "TR" or "DE" or "NL" ? null : $"no shipping to {order.ShippingCountry}",
order => order.Lines.Sum(line => line.Quantity) > 50 ? "bulk orders go through sales" : null
];
// Chain of Responsibility: ilk hatada durur (Select deferred execution ile çalışır)
public static string? FirstError(OrderDraft order) =>
Rules.Select(rule => rule(order)).FirstOrDefault(error => error is not null);
// Pipeline: bütün kurallar çalışır, bütün hatalar toplanır
public static IReadOnlyList<string> AllErrors(OrderDraft order) =>
[.. Rules.Select(rule => rule(order)).OfType<string>()];
}
public static class ValidationChainDemo
{
public static void Run()
{
OrderDraft draft = new("Ada", "US", [new("978-0441013593", 0), new("978-0547928227", 60)]);
Console.WriteLine($"first error: {OrderRules.FirstError(draft)}");
Console.WriteLine($"all errors: {string.Join(" | ", OrderRules.AllErrors(draft))}");
Console.WriteLine($"valid order: {OrderRules.FirstError(new("Alan", "DE", [new("978-0441013593", 2)])) ?? "no errors"}");
}
}
first error: quantities must be positive
all errors: quantities must be positive | no shipping to US | bulk orders go through sales
valid order: no errors
FirstError klasik Chain of Responsibility: Select deferred execution ile çalıştığı için FirstOrDefault ilk hatayı bulduğu anda kalan kurallar hiç çalışmıyor. Pahalı bir kural (stok servisine bir çağrı, bir veritabanı sorgusu) chain'in sonundaysa bu fark ölçülebilir. AllErrors ise aynı kurallarla bir pipeline: hepsi çalışıyor, hepsinin sonucu toplanıyor. Hangisini seçeceğin bir UX (User Experience) kararı: formda kullanıcıya bütün hataları birden göstermek istiyorsan pipeline, "ilk engel yeter" diyen bir API ya da pahalı kontroller için chain. FluentValidation gibi kütüphanelerin CascadeMode ayarı tam bu iki davranış arasında seçim yapmanı sağlar.
Sık Düşülen Tuzaklar
Chain'in sonundan düşen istek. Hiçbir handler'ın ele almadığı bir istek, açıkça tasarlanmamışsa sessizce kaybolur. Klasik biçimde chain'in son handler'ı ne döndürecek, DI biçiminde FirstOrDefault null verdiğinde ne olacak? ASP.NET Core bu sorunun cevabını kendisi verir: hiçbir endpoint eşleşmezse pipeline'ın sonu 404 döndürür. Kendi chain'inde de bir "terminal handler" ya da açık bir red cevabı olsun.
Sıraya gizli bağımlılık. Middleware'de UseAuthentication'ı UseAuthorization'dan sonra yazmak, DelegatingHandler'larda retry handler'ını cache'in içine koymak, DI'da approver'ları yeniden sıralamak; hepsi derlenen ama yanlış çalışan kod üretir. Sıra bir kural taşıyorsa ya açıkça ifade et (Order, isimlendirilmiş tek bir kurulum method'u) ya da bütün chain'i test et.
Handler'da state tutmak. Örnekteki CachingHandler cache'i kendi field'ında tutuyor ve demo bu yüzden çalışıyor. Ama IHttpClientFactory handler'ları havuzda tutar ve varsayılan olarak iki dakikada bir yeniler; o cache handler'la birlikte habersizce sıfırlanır ve aynı handler instance'ı aynı anda birden fazla istek tarafından kullanılabileceği için Dictionary thread-safe de değil. Gerçek bir cache için IMemoryCache ya da HybridCache enjekte et.
Uzun chain'de debug. On iki middleware'li bir pipeline'da "bu 401 nereden geldi?" sorusunun cevabı stack trace'te değil, sırada. Short-circuit eden her handler'ın neden durduğunu loglaması, bu soruyu dakikalara indirir.
Döngü. Klasik biçimde a.SetNext(b); b.SetNext(a); yazan bir kurulum kodu, ele alınmayan ilk istekte StackOverflowException ile process'i düşürür. Successor referansı yerine bir koleksiyon üzerinde dönen DI biçimi bu tür hatayı baştan ortadan kaldırır.
Ne Zaman Kullanma
Handler sayısı sabit ve küçükse, sırası hiç değişmeyecekse ve her biri farklı bir tipte istek ele alıyorsa, bir switch expression daha okunaklıdır: request.Percent switch { <= 5 => ..., <= 20 => ..., _ => ... } bütün kuralı tek ekranda gösterir. Chain of Responsibility'nin değeri, handler'ların bağımsız olarak eklenip çıkarılabildiği, farklı ekiplerin farklı handler'lara sahip olduğu ya da chain'in configuration ile şekillendiği yerde ortaya çıkar. Her isteğin tam olarak bir alıcısı varsa ve bu alıcı derleme zamanında belliyse, araya chain koymak yalnızca go-to-definition'ı kırar.
Küçük Bir Deneme
★☆☆ İndirim chain'ine yüzde 10'a kadar onay veren bir RegionalManager ekle. Klasik biçimde ve DI biçiminde kaç dosyaya dokunman gerekti?
★★☆ IDiscountApprover'a int Order ekle ve DiscountApprovalChain'i kayıt sırasından bağımsız hâle getir. Ardından "finance director first" senaryosunu yakalayacak tek bir xUnit testi yaz.
★★★ Middleware örneğine, X-Api-Key değeri geçersizse 403 döndüren ve bunu loglayan üçüncü bir middleware ekle. Hangi sıraya koyarsan timing middleware'i 403'ü görür, hangi sırada görmez? Short-circuit eden bir middleware'in hangi durumlarda next'ten sonra cevaba yazamayacağını (Response.HasStarted) araştır.
Bir sonraki yazıda isteği bir object'e çeviren pattern'e geçiyoruz: Command. Bookstore'un katalog editöründe undo/redo stack'ini kuracak, command'ları JSON'a (JavaScript Object Notation) serialize edip bir queue üzerinden background'da çalıştıracak ve CQRS'teki (Command Query Responsibility Segregation) "command" ile GoF'un command'ının aynı şey olup olmadığını konuşacağız.