Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Vertical Slice Architecture — #03

Mediator'lı Slice: MediatR, Mediator (Source Generator) ve Wolverine Karşılaştırması

Mert Özen 7 Kas 2026 15 dk 3 görüntülenme
Mediator'lı Slice: MediatR, Mediator (Source Generator) ve Wolverine Karşılaştırması

Slice'larda mediator'ın asıl getirisi giriş noktasından bağımsız bir pipeline'dır; Kasım 2026 itibarıyla MediatR 14.3 RPL 1.5 ya da ticari lisanslı, Mediator 3.0.2 ve Wolverine 6.62 MIT lisanslı. Aynı PlaceOrder slice'ını üç araçla ve mediator'sız bir endpoint filter'la yazıp dispatch maliyetini, ilk istek süresini, Native AOT davranışını ve her aracın gerçek kurulum tuzaklarını 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

Ekip, MediatR'ın lisans değişikliğinden sonra source generator tabanlı Mediator library'sine geçmeye karar veriyor. Slice'ın handler'ı önceki yazıdakiyle aynı: BookstoreDbContext'i constructor'dan alıyor. Kayıt da README'deki gibi kısa:

builder.Services.AddMediator(options =>
{
    options.PipelineBehaviors = [typeof(LoggingBehavior<,>)];
});

Library'nin dokümanına göre handler'ların varsayılan lifetime'ı singleton; DbContext ise scoped. Tahmin et: uygulamayı önce Development'ta, sonra Production'da açıp aynı anda 40 sipariş isteği gönderirsen ne olur? İki ortamda aynı şey mi olur?

Mediator Ne Kazandırır, Ne Kaybettirir

Önceki yazıda slice'ı hiçbir library kullanmadan kurduk: endpoint, handler class'ını doğrudan inject edip çağırıyordu. Bir mediator araya bir dispatcher koyar: endpoint handler'ı tanımaz, request object'ini mediator'a verir, mediator da request'in tipine bakıp doğru handler'ı bulur. GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) Mediator pattern'iyle bu dispatcher'ın farkını ve "MediatR'a gerçekten ihtiyacım var mı?" sorusunun genel cevabını Design Patterns serisinin "Mediator Pattern: Object'ler Arası Konuşmayı Merkezileştirmek ve 'MediatR'a Gerçekten İhtiyacım Var mı?'" yazısında konuştuk. Bu yazı daha dar bir soruya bakıyor: vertical slice'larda mediator'ın pratik getirisi ne ve üç popüler aracın arasında nasıl seçim yapılır?

Kazanç üç başlıkta toplanıyor. Birincisi, tek tip handler sözleşmesi: her slice'ın handler'ı aynı interface'i implement ediyor, yeni gelen biri yapıyı tek slice'tan öğreniyor. İkincisi, pipeline: logging, validation, transaction gibi işler bütün handler'ların etrafına tek bir yerde sarılıyor. Üçüncüsü ve en az konuşulanı, giriş noktasından bağımsızlık: aynı pipeline, request'i bir HTTP (Hypertext Transfer Protocol) endpoint'i de gönderse, bir background job ya da bir message consumer da gönderse çalışıyor.

Kayıp ise indirection. Endpoint'te sender.Send(command) satırında IDE'de (integrated development environment) "Go to Definition" yaptığında handler'a değil ISender.Send'e gidersin; handler'ı bulmak için IRequestHandler<PlaceOrderCommand, ...>'i aramak gerekir. Eksik bir handler derleme hatası değil, çoğu araçta runtime hatasıdır. VSA (Vertical Slice Architecture) bu bedeli kısmen hafifletiyor: command, handler ve endpoint aynı klasörde, çoğu zaman aynı dosyada duruyor; aradığın şey bir ekran aşağıda.

Üç aracı aynı slice üzerinde deneyeceğiz. Ortak kod (entity'ler, BookstoreDbContext ve fiyatları çekip siparişi kuran BuildOrderAsync extension method'u) bir class library'de; her varyant yalnızca slice'ı ve pipeline'ı yazıyor. Pipeline'da tek bir cross-cutting iş var: her request'in süresini log'lamak.

MediatR 14.3 ile Slice

using Bookstore.Common;
using MediatR;

namespace Bookstore.Features.Orders;

public sealed record PlaceOrderCommand(int CustomerId, List<PlaceOrderLine> Lines) : IRequest<PlaceOrderResponse?>;

public sealed class PlaceOrderHandler(BookstoreDbContext db) : IRequestHandler<PlaceOrderCommand, PlaceOrderResponse?>
{
    public async Task<PlaceOrderResponse?> Handle(PlaceOrderCommand command, CancellationToken ct)
    {
        Order? order = await db.BuildOrderAsync(command.CustomerId, command.Lines, ct);
        if (order is null)
            return null;
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return new PlaceOrderResponse(order.Id, order.Items.Sum(i => i.UnitPrice * i.Quantity));
    }
}

public static class PlaceOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app) =>
        app.MapPost("/orders", async (PlaceOrderCommand command, ISender sender, CancellationToken ct) =>
            await sender.Send(command, ct) is { } response
                ? Results.Created($"/orders/{response.OrderId}", response)
                : Results.ValidationProblem(new Dictionary<string, string[]> { ["Lines"] = ["Unknown book"] }));
}
using System.Diagnostics;
using MediatR;

namespace Bookstore.Pipeline;

// Her request için tek bir yerde çalışan cross-cutting iş
public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        long start = Stopwatch.GetTimestamp();
        TResponse response = await next(ct);
        logger.LogInformation("{Request} handled in {Elapsed:F1} ms", typeof(TRequest).Name, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
        return response;
    }
}
using Bookstore.Common;
using Bookstore.Features.Orders;
using Bookstore.Pipeline;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddBookstoreDb("mediatr.db");
builder.Services.AddMediatR(cfg =>
{
    cfg.LicenseKey = builder.Configuration["MediatR:LicenseKey"];
    cfg.RegisterServicesFromAssemblyContaining<PlaceOrderHandler>();
    cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

WebApplication app = builder.Build();
app.Services.ResetDatabase();
PlaceOrderEndpoint.Map(app);
app.Run();

Üç sipariş ve bir bilinmeyen kitapla denediğimde log:

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.
info: Bookstore.Pipeline.LoggingBehavior[0]
      PlaceOrderCommand handled in 96.5 ms
info: Bookstore.Pipeline.LoggingBehavior[0]
      PlaceOrderCommand handled in 13.7 ms
info: Bookstore.Pipeline.LoggingBehavior[0]
      PlaceOrderCommand handled in 1.3 ms
info: Bookstore.Pipeline.LoggingBehavior[0]
      PlaceOrderCommand handled in 1.9 ms

İlk satır lisans uyarısı; LicenseKey boş olduğu için açılışta bir kez yazılıyor. Lisansın ayrıntıları aşağıda. Eski örneklerden farklı bir ayrıntı: 14.3'te RequestHandlerDelegate<TResponse> bir CancellationToken alıyor, behavior'da parametresiz next() yerine next(ct) yazıyoruz.

Mediator (Source Generator) ile Slice

martinothamar/Mediator, MediatR'a çok benzeyen bir API'yi (Application Programming Interface) reflection yerine build sırasında üretilen kodla sunuyor. Slice neredeyse aynı; farklar dönüş tipinde ve behavior'ın delegate'inde:

using Bookstore.Common;
using Mediator;

namespace Bookstore.Features.Orders;

public sealed record PlaceOrderCommand(int CustomerId, List<PlaceOrderLine> Lines) : ICommand<PlaceOrderResponse?>;

public sealed class PlaceOrderHandler(BookstoreDbContext db) : ICommandHandler<PlaceOrderCommand, PlaceOrderResponse?>
{
    public async ValueTask<PlaceOrderResponse?> Handle(PlaceOrderCommand command, CancellationToken ct)
    {
        Order? order = await db.BuildOrderAsync(command.CustomerId, command.Lines, ct);
        if (order is null)
            return null;
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return new PlaceOrderResponse(order.Id, order.Items.Sum(i => i.UnitPrice * i.Quantity));
    }
}
using System.Diagnostics;
using Mediator;

namespace Bookstore.Pipeline;

// Her message için tek bir yerde çalışan cross-cutting iş
public sealed class LoggingBehavior<TMessage, TResponse>(ILogger<LoggingBehavior<TMessage, TResponse>> logger)
    : IPipelineBehavior<TMessage, TResponse> where TMessage : notnull, IMessage
{
    public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken ct)
    {
        long start = Stopwatch.GetTimestamp();
        TResponse response = await next(message, ct);
        logger.LogInformation("{Message} handled in {Elapsed:F1} ms", typeof(TMessage).Name, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
        return response;
    }
}

Şimdi açılıştaki soru. Development'ta uygulama hiç açılmadı:

Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Bookstore.Features.Orders.PlaceOrderHandler Lifetime: Singleton ImplementationType: Bookstore.Features.Orders.PlaceOrderHandler': Cannot consume scoped service 'Bookstore.Common.BookstoreDbContext' from singleton 'Bookstore.Features.Orders.PlaceOrderHandler'.) ...

ASP.NET Core, Development ortamında scope validation'ı açık tutuyor ve singleton bir handler'ın scoped DbContext'i yakalamasını açılışta reddediyor. Production'da bu validation varsayılan olarak kapalı. Aynı kodu Production'da açıp 40 eşzamanlı istek gönderdim:

   1 201
  39 500
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

Singleton handler, uygulamanın ömrü boyunca tek bir DbContext'i tutuyor ve bütün request'ler onu paylaşıyor. Tek tek denenen her istek çalıştığı için bu hata yük altına kadar saklanabilir. ASP.NET Core Web API serisinin "ASP.NET Core'da Dependency Injection: Lifetime'lar ve Captive Dependency Tuzağı" yazısındaki captive dependency'nin bir mediator üzerinden gelen hâli. Çözüm tek satır:

using Bookstore.Common;
using Bookstore.Features.Orders;
using Bookstore.Pipeline;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddBookstoreDb("mediator.db");
builder.Services.AddMediator(options =>
{
    // Varsayılan Singleton; handler'lar DbContext kullandığı için Scoped olmalı
    options.ServiceLifetime = ServiceLifetime.Scoped;
    options.PipelineBehaviors = [typeof(LoggingBehavior<,>)];
});

WebApplication app = builder.Build();
app.Services.ResetDatabase();
PlaceOrderEndpoint.Map(app);
app.Run();

Source generator'ın bir getirisi de eksik handler'ı derlemede söylemesi. Handler'ı olmayan bir CancelOrder command'ı yazdığımda Mediator build'de uyardı, MediatR ise ancak çalışırken hata verdi:

Program.cs(10,22): warning MSG0005: MediatorGenerator found message without any registered handler: CancelOrder
Unhandled exception. System.InvalidOperationException: No service for type 'MediatR.IRequestHandler`2[CancelOrder,System.String]' has been registered.

Wolverine ile Slice

Wolverine bir mediator'dan fazlası: asıl kimliği bir messaging framework'ü, mediator kullanımı bunun bir alt kümesi. Yaklaşımı da farklı: handler'lar interface implement etmiyor, convention'la bulunuyor.

using Bookstore.Common;
using Wolverine;

namespace Bookstore.Features.Orders;

public sealed record PlaceOrderCommand(int CustomerId, List<PlaceOrderLine> Lines);

// Interface yok: Wolverine class'ı adından, method'u Handle adından bulur
public static class PlaceOrderHandler
{
    public static async Task<PlaceOrderResponse?> Handle(PlaceOrderCommand command, BookstoreDbContext db, CancellationToken ct)
    {
        Order? order = await db.BuildOrderAsync(command.CustomerId, command.Lines, ct);
        if (order is null)
            return null;
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return new PlaceOrderResponse(order.Id, order.Items.Sum(i => i.UnitPrice * i.Quantity));
    }
}

public static class PlaceOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app) =>
        app.MapPost("/orders", async (PlaceOrderCommand command, IMessageBus bus, CancellationToken ct) =>
            await bus.InvokeAsync<PlaceOrderResponse?>(command, ct) is { } response
                ? Results.Created($"/orders/{response.OrderId}", response)
                : Results.ValidationProblem(new Dictionary<string, string[]> { ["Lines"] = ["Unknown book"] }));
}
using System.Diagnostics;
using Wolverine;

namespace Bookstore.Pipeline;

// Convention'lı middleware: Before handler'dan önce, Finally sonra çalışır
public static class LoggingMiddleware
{
    public static long Before() => Stopwatch.GetTimestamp();

    public static void Finally(long start, Envelope envelope, ILogger logger) =>
        logger.LogInformation("{Message} handled in {Elapsed:F1} ms", envelope.MessageType, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
}
using Bookstore.Common;
using Bookstore.Features.Orders;
using Bookstore.Pipeline;
using Wolverine;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Wolverine'in ürettiği kod DbContextOptions'ı singleton bekliyor
builder.Services.AddBookstoreDb("wolverine.db", optionsLifetime: ServiceLifetime.Singleton);
builder.Host.UseWolverine(opts =>
{
    // Yalnızca in-process mediator olarak: queue, transport ve kalıcı mesaj yok
    opts.Durability.Mode = DurabilityMode.MediatorOnly;
    opts.Policies.AddMiddleware(typeof(LoggingMiddleware));
});

WebApplication app = builder.Build();
app.Services.ResetDatabase();
PlaceOrderEndpoint.Map(app);
app.Run();

Buraya ilk denemede gelemedim. Yalnızca WolverineFx paketiyle uygulama açılmadı:

System.InvalidOperationException: Wolverine is running in TypeLoadMode.Dynamic, which compiles handler/middleware code at runtime, but no IAssemblyGenerator (Roslyn) is registered. Core WolverineFx no longer ships the runtime compiler. Either add the 'WolverineFx.RuntimeCompilation' NuGet package (it auto-registers when referenced, or call opts.UseRuntimeCompilation() in UseWolverine(...)), or pre-generate code with 'dotnet run -- codegen write' and set opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static. See https://wolverinefx.net/guide/codegen.html (GH-2876).

Wolverine her handler için bir adapter class'ı üretiyor; 6.0'dan beri bu kodu runtime'da derleyen Roslyn ayrı bir pakette. WolverineFx.RuntimeCompilation'ı ekleyince uygulama açıldı ama ilk istek bu sefer şununla döndü:

Wolverine.Configuration.InvalidServiceLocationException: Found service locations while generating code for Message Handler for Bookstore.Features.Orders.PlaceOrderCommand, but ServiceLocationPolicy.NotAllowed is in effect (it is the default).
...
The service registration for Microsoft.EntityFrameworkCore.DbContextOptions<Bookstore.Common.BookstoreDbContext> is an 'opaque' lambda factory with the Scoped lifetime and requires service location

Üretilen kodun nasıl göründüğü bu hatayı açıklıyor. dotnet run -- codegen write komutu kodu dosyaya yazıyor; bizim handler'ın adapter'ından bir parça:

public override async System.Threading.Tasks.Task HandleAsync(Wolverine.Runtime.MessageContext context, System.Threading.CancellationToken cancellation)
{
    await using var bookstoreDbContext = new Bookstore.Common.BookstoreDbContext(_dbContextOptionsOfBookstoreDbContext);
    // The actual message body
    var placeOrderCommand = (Bookstore.Features.Orders.PlaceOrderCommand)context.Envelope.Message;

    var result_of_Before = Bookstore.Pipeline.LoggingMiddleware.Before();
    try
    {
        Wolverine.Runtime.WolverineTracing.MessageActivity(context.Envelope)?.SetTag("message.handler", "Bookstore.Features.Orders.PlaceOrderHandler");
        Wolverine.Runtime.WolverineTracing.MessageActivity(context.Envelope)?.SetTag("handler.type", "Bookstore.Features.Orders.PlaceOrderHandler");

        // The actual message execution
        var outgoing1 = await Bookstore.Features.Orders.PlaceOrderHandler.Handle(placeOrderCommand, bookstoreDbContext, cancellation).ConfigureAwait(false);

        // Outgoing, cascaded message
        await context.EnqueueCascadingAsync(outgoing1).ConfigureAwait(false);
    }
    finally
    {
        Bookstore.Pipeline.LoggingMiddleware.Finally(result_of_Before, context.Envelope, ((Microsoft.Extensions.Logging.ILogger)_loggerForMessage));
    }
}

Wolverine DbContext'i container'dan istemiyor, options'tan kendisi new'liyor; options'ın da constructor'a bir kez verilebilmesi için singleton olması gerekiyor. AddDbContext'in optionsLifetime parametresi bunun için. Middleware de bir decorator değil, üretilen kodun içine satır olarak yerleşiyor: reflection ya da delegate zinciri yok. Bedeli, bu kodun ilk kullanımda derlenmesi; aşağıdaki ölçümlerde göreceğiz. Bir de küçük ama şaşırtıcı bir kural: benchmark için int dönen bir handler yazdığımda Wolverine onu handler saymadı. DescribeHandlerMatch'in açıklaması "EXCLUDED -- Returns a primitive type"; dönüş değerleri cascading message olarak ele alındığı için primitive tipler dışarıda kalıyor.

Mediator'sız Pipeline: Endpoint Filter

Aynı logging'i hiçbir library olmadan, ASP.NET Core'un endpoint filter'ıyla da yazabilirsin. Handler önceki yazıdaki gibi düz bir class; filter bütün slice'ların bağlı olduğu gruba ekleniyor:

using System.Diagnostics;

namespace Bookstore.Pipeline;

// Mediator yok: aynı cross-cutting iş bir endpoint filter'ında
public sealed class LoggingFilter(ILogger<LoggingFilter> logger) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        long start = Stopwatch.GetTimestamp();
        object? result = await next(context);
        logger.LogInformation("{Endpoint} handled in {Elapsed:F1} ms", context.HttpContext.GetEndpoint()?.DisplayName, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
        return result;
    }
}
WebApplication app = builder.Build();
app.Services.ResetDatabase();
RouteGroupBuilder slices = app.MapGroup("").AddEndpointFilter<LoggingFilter>();
PlaceOrderEndpoint.Map(slices);
app.Run();
info: Bookstore.Pipeline.LoggingFilter[0]
      HTTP: POST /orders handled in 99.0 ms
info: Bookstore.Pipeline.LoggingFilter[0]
      HTTP: POST /orders handled in 13.4 ms

Sonuç aynı, sınır farklı: filter yalnızca HTTP'den gelen istekleri görür. Aynı handler'ı bir background job'dan çağırdığında filter çalışmaz. Giriş noktası tek ise (çoğu API'de öyle) bu bir eksik değil; birden çok giriş noktası varsa ya bir mediator'ın pipeline'ına ya da handler'ın etrafına decorator'a ihtiyacın var. Decorator yolunu, Scrutor ile ve MediatR'sız, Clean, Onion ve Hexagonal Architecture serisinin "Application Layer ve Use Case'ler: MediatR Olmadan Handler, Validation ve Transaction Sınırı" yazısında kurduk; filter türlerinin ayrıntısı da ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Filter'lar: Action Filter, Result Filter ve ServiceFilter" yazısında.

Ölçümler: Dispatch Maliyeti ve İlk İstek

Mediator'ların performans karşılaştırması genellikle dispatch maliyetine bakar. BenchmarkDotNet 0.15.8 ile, veritabanı olmadan, bir sayıyı bir artırıp record'a saran boş bir handler'ı dört yolla çağırdım (Apple M5 (10 çekirdekli) bir Mac, .NET 10.0.13):

| Method    | Mean       | Error     | StdDev    | Ratio | RatioSD | Gen0   | Allocated | Alloc Ratio |
|---------- |-----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
| Direct    |  10.242 ns | 1.7477 ns | 1.6348 ns |  1.02 |    0.22 | 0.0115 |      96 B |        1.00 |
| MediatR   |  35.878 ns | 0.5117 ns | 0.4536 ns |  3.59 |    0.55 | 0.0296 |     248 B |        2.58 |
| Mediator  |   7.641 ns | 0.0685 ns | 0.0641 ns |  0.76 |    0.12 | 0.0057 |      48 B |        0.50 |
| Wolverine | 197.557 ns | 0.4873 ns | 0.3804 ns | 19.75 |    3.01 | 0.0248 |     208 B |        2.17 |

Mediator'ın doğrudan çağrıdan bile hızlı görünmesi bir ölçüm artefaktı: Direct Task<T> dönüyor, Mediator ValueTask<T>; aradaki fark dispatcher'dan değil dönüş tipinden. Asıl ders sayıların büyüklüğünde: en yavaşı bile 0,2 mikrosaniye. Yukarıdaki log'larda bir sipariş isteği 1–14 milisaniye sürüyor; dispatch maliyeti bunun binde birinden az. Slice'ta mediator seçerken bu tablo neredeyse hiçbir zaman belirleyici değil.

Daha görünür fark ilk istekte. Her varyantı yeniden başlatıp aynı siparişi üç kez gönderdim (curl'ün time_total değeri, saniye):

WithFilter            0.106  0.015  0.002
WithMediatR           0.111  0.014  0.003
WithMediator          0.110  0.015  0.002
WithWolverine         0.550  0.015  0.002
WithWolverineStatic   0.116  0.014  0.002

İlk istekteki 0,1 saniye herkeste ortak (EF Core'un model'i kurması, JIT (just-in-time) derleme); Wolverine'in Dynamic modunda buna handler kodunun Roslyn ile derlenmesi ekleniyor. Kodu codegen write ile önceden üretip TypeLoadMode.Static'e geçince fark kayboluyor; Wolverine'in dokümanı da production için bunu öneriyor.

Lisans Durumu (Kasım 2026)

Bu bölüm zamana duyarlı; bilgiler 8 Kasım 2026'da NuGet'teki paketlerin kendisi, lisans dosyaları ve projelerin resmî sayfalarıyla kontrol edildi.

MediatR: 13.0.0 (2 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; güncel sürüm 14.3.0. Paketin içindeki LICENSE.md'ye göre RPL 1.5 ile kullanıyorsan, bu kodla geliştirdiğin yazılımın kaynağını da RPL şartlarıyla açman gerekiyor; bunu istemiyorsan ticari lisans alıyorsun. Ticari tarafta ücretsiz bir Community edition var: yıllık geliri 5 milyon doların altında olan, hiç 10 milyon dolardan fazla dış yatırım almamış ve kamu kurumu olmayan kuruluşlar için. Ücretli katmanlar geliştirici sayısına göre fiyatlanıyor. Lisans anahtarı her iki durumda da isteniyor; anahtar yoksa çalışma engellenmiyor, yukarıdaki uyarı log'lanıyor. 12.5.0 (1 Nisan 2025), Apache 2.0 lisanslı son sürüm ve o lisansla kalıyor.

Mediator (martinothamar): güncel kararlı sürüm 3.0.2, paketteki lisans dosyası MIT (Massachusetts Institute of Technology'nin adını taşıyan izin verici açık kaynak lisansı). 3.1 sürüm adayı aşamasında. Wolverine (WolverineFx): güncel sürüm 6.62.0, NuGet'teki lisans ifadesi MIT. FastEndpoints gibi farklı bir yaklaşımı 06. yazıda ele alacağız.

Karşılaştırma

ÖlçütMediator'sız (handler + filter)MediatR 14.3.0Mediator 3.0.2Wolverine 6.62.0
LisansYok (framework)RPL 1.5 ya da ticari; Community edition koşullu ücretsizMITMIT
Dispatch (bu makinede)10 ns, 96 B36 ns, 248 B8 ns, 48 B198 ns, 208 B
İlk istek0,106 sn0,111 sn0,110 snDynamic 0,550 sn, Static 0,116 sn
Native AOT (küçük console denemesi)Denemedim; library eklemiyorIL2104 (trim) ve IL3053 (AOT analizi) uyarılarıyla yayınlandı, açılışta InvalidOperationExceptionUyarısız yayınlandı ve çalıştıDokümana göre Static codegen ön koşul; denemedim
Eksik handlerClass yoksa derleme hatası, kayıt eksikse runtime hatası (02. yazı)Runtime hatasıBuild uyarısı MSG0005 (generator uyarısı), runtime hatasıRuntime hatası
PipelineEndpoint filter (yalnız HTTP) ya da decoratorIPipelineBehaviorIPipelineBehaviorConvention'lı middleware, üretilen koda gömülü
Öğrenme eğrisiEn düşükDüşük, en yaygın örneklerDüşük, MediatR'a çok benzerDaha yüksek: convention'lar, codegen, messaging kavramları

Native AOT satırı için her iki library'yle aynı handler ve behavior'ı PublishAot açık bir console uygulamasında yayınladım. MediatR, assembly scan ve open generic kayıtlar yüzünden trimmer'ın göremediği tiplere ihtiyaç duyuyor; ek trimming ayarlarıyla çalıştırmak mümkün olabilir ama kutudan çıktığı hâliyle çalışmadı. Mediator'ın üretilen kodu her şeyi derleme zamanında bağladığı için sorun çıkmadı.

Bu serinin varsayılanı, 02. yazıdaki mediator'sız slice. Birden çok giriş noktası olup aynı pipeline'ı paylaşmak gerektiğinde ilk tercihim Mediator: MIT lisanslı, en hızlısı, AOT uyumlu ve MediatR'dan geçişi kolay. Mesajlaşma zaten gündemdeyse (outbox, queue, saga) Wolverine'i yalnızca mediator olarak değil o platformun parçası olarak değerlendirmek daha doğru; ilerideki Messaging ve Event-Driven Architecture serisinde bu soruya döneceğiz. MediatR'da kalmak, Community edition'a uyan ya da lisansı satın almaya hazır ve büyük bir MediatR kod tabanı olan ekipler için makul bir karar.

Sık Düşülen Tuzaklar

Lifetime'ı varsayılan bırakmak. Yukarıda gördüğümüz gibi Mediator'ın varsayılanı singleton ve Production'da scope validation kapalı; captive bir DbContext ancak yük altında patlar. Hangi library'yi seçersen seç, handler'ların lifetime'ını DbContext'inkiyle eşleştir ve integration test'leri Development ayarlarıyla koş.

Pipeline'ı HTTP'nin yerine koymak. Mediator pipeline'ında authorization, input validation ya da HTTP cevabı üretmek, slice'ı iki kez filtreden geçirmek demek. ASP.NET Core zaten validation (AddValidation), authorization ve exception handling sunuyor; mediator pipeline'ını giriş noktasından bağımsız olması gereken işlere (transaction, audit, domain event'leri yayma) sakla.

Slice'ları mediator üzerinden birbirine bağlamak. CancelOrder handler'ının içinden sender.Send(new RestockBooks(...)) çağırmak, mediator olduğu için "gevşek bağlı" görünür ama iki slice'ı aynı transaction'a ve aynı hata akışına bağlar. Bu konuya 04. yazıda ayrıntılı döneceğiz.

Benchmark'a göre seçmek. Nanosaniyeler arasındaki fark gerçek ama bir veritabanı çağrısının yanında görünmez. Seçimi lisans, AOT ihtiyacı, ekibin bildiği araç ve mesajlaşma planları belirlemeli.

Küçük Bir Deneme

★☆☆ Mediator varyantında ServiceLifetime satırını sil ve uygulamayı ASPNETCORE_ENVIRONMENT=Production ile aç. Önce tek tek, sonra eşzamanlı birkaç istek gönder; hatanın ne zaman göründüğünü not et.

★★☆ Logging behavior'ının yanına bir transaction behavior'ı ekle: handler'ı bir IDbContextTransaction içinde çalıştırsın, exception'da rollback yapsın. Aynı işi endpoint filter ile yazmaya çalış ve bir background job'dan çağrıldığında ne olduğunu gözle.

★★★ Wolverine varyantını TypeLoadMode.Static'e geçir, codegen write ile kodu üret ve ilk istek süresini ölç. Sonra LoggingMiddleware'e yeni bir parametre ekle ve kodu yeniden üretmeden çalıştır; hangi hatayı alıyorsun?

Mediator seçimi slice'ın içini değiştirmiyor; asıl zor soru slice'lar çoğaldığında başlıyor. Bir sonraki yazıda slice'lar arası paylaşımı konuşuyoruz: neyin ortak olacağı, neyin kopyalanmasının daha iyi olduğu ve iş kurallarının handler'lardan domain'e ne zaman taşınacağı.