مقدمه
الگوی واسطه (Mediator Pattern) از طریق کتابخانهای مانند MediatR به ابزاری محبوب برای مدیریت ارتباطات بین لایههای مختلف برنامه تبدیل شده است. این کتابخانه با ارائه راهحلی تمیز و منعطف برای ارسال دستورات (Commands)، اعلانها (Notifications) و افزودن رفتارهایی مانند لاگگیری (Logging) یا اعتبارسنجی (Validation)، پیچیدگیهای معماری نرمافزار را کاهش میدهد. با این حال، وابستگی به کتابخانههای خارجی ممکن است چالشهایی مانند بهروزرسانیهای غیرمنتظره، پیچیدگیهای اضافی یا مسائل مربوط به مجوز (Licensing) را به همراه داشته باشد.
در مقابل، System.Threading.Channels، که بخشی از کتابخانه استاندارد پایه (Base Class Library - BCL) داتنت است، جایگزینی بومی، سبک و کارآمد ارائه میدهد. این ابزار که در اصل برای سناریوهای تولیدکننده-مصرفکننده (Producer-Consumer) با توان عملیاتی بالا طراحی شده، یک صف پیامرسانی در حافظه (In-Memory Message Queue) با عملکرد بالا و بدون نیاز به وابستگی خارجی فراهم میکند. هدف این مقاله، آموزش نحوه جایگزینی MediatR با System.Threading.Channels برای مدیریت ارتباطات درونبرنامهای است. با بررسی مزایا، کدها و مثالهای عملی، این مقاله به توسعهدهندگان کمک میکند تا با استفاده از این ابزار بومی، معماری سادهتر و شفافتری پیادهسازی کنند.
چرا به جایگزینی برای MediatR نیاز داریم؟
MediatR با استفاده از الگوی واسطه، امکان ارسال پیامها بین اجزای مختلف برنامه را بهصورت غیرمستقیم فراهم میکند. این کتابخانه با تزریق خودکار وابستگیها (Dependency Injection) و پشتیبانی از رفتارهای پویا (مانند Pipeline Behaviors)، توسعهدهندگان را از پیچیدگیهای مدیریت ارتباطات رها میسازد. اما این قابلیتها هزینههایی نیز دارند:
- وابستگی خارجی: نیاز به نصب بستههای NuGet و بهروزرسانی مداوم آنها.
- پیچیدگیهای پنهان: استفاده از بازتاب (Reflection) برای یافتن و اجرای هندلرها (Handlers) ممکن است شفافیت کد را کاهش دهد.
- عملکرد در بار بالا: در سناریوهای با توان عملیاتی بالا، سربارهای MediatR ممکن است محسوس باشد.
در مقابل، System.Threading.Channels با ارائه یک صف پیامرسانی ساده و بومی، این چالشها را برطرف میکند. این ابزار بدون نیاز به وابستگی خارجی، عملکردی بهینه و شفافیت کامل در کد ارائه میدهد. همچنین، به دلیل استفاده در هسته ASP.NET Core، از نظر عملکرد و پایداری کاملاً آزمایششده است.
معرفی System.Threading.Channels
System.Threading.Channels در فضای نام (Namespace) System.Threading.Channels قرار دارد و بهعنوان یک مکانیزم برای انتقال دادههای ناهمگام (Asynchronous) بین وظایف (Tasks) عمل میکند. این ابزار مشابه صفهای سنتی است، اما با قابلیتهای پیشرفتهتر مانند پشتیبانی از فشار بازگشتی (Backpressure) و مدیریت لغو (Cancellation). کانالها در دو نوع اصلی ارائه میشوند:
- بدون محدودیت (Unbounded): هیچ محدودیتی در ظرفیت صف وجود ندارد و دادهها بدون توقف ذخیره میشوند.
- محدود (Bounded): ظرفیت صف محدود است و در صورت پر شدن، فشار بازگشتی اعمال میشود تا تولیدکننده (Producer) منتظر بماند.
این انعطافپذیری، کانالها را برای سناریوهای مختلف، از ارسال دستورات ساده تا پردازش اعلانهای پیچیده، مناسب میسازد.
پیادهسازی یک سیستم پیامرسانی با System.Threading.Channels
برای جایگزینی MediatR، میتوان یک سیستم پیامرسانی ساده با استفاده از System.Threading.Channels پیادهسازی کرد. در ادامه، گامهای اصلی این فرآیند با مثالهای کد توضیح داده میشود.
1. ایجاد یک صف در حافظه (In-Memory Queue)
اولین گام، ایجاد یک کلاس برای مدیریت صف پیامها است. کد زیر یک نمونه ساده از InMemoryMessageQueue را نشان میدهد:
internal sealed class InMemoryMessageQueue
{
private readonly Channel<IIntegrationEvent> _channel = Channel.CreateUnbounded<IIntegrationEvent>();
public ChannelReader<IIntegrationEvent> Reader => _channel.Reader;
public ChannelWriter<IIntegrationEvent> Writer => _channel.Writer;
}در این کد:
- Channel.CreateUnbounded یک کانال بدون محدودیت ایجاد میکند.
- ChannelReader و ChannelWriter به ترتیب برای خواندن و نوشتن پیامها استفاده میشوند.
- IIntegrationEvent یک رابط (Interface) است که رویدادهای یکپارچهسازی (Integration Events) را تعریف میکند.
این ساختار ساده، یک صف پیامرسانی thread-safe با پشتیبانی از فشار بازگشتی و لغو فراهم میکند.
2. ایجاد یک سرویس انتشار رویداد (Event Bus)
برای انتشار و پردازش رویدادها، یک سرویس EventBus تعریف میکنیم:
internal sealed class EventBus : IEventBus
{
private readonly InMemoryMessageQueue _queue;
public EventBus(InMemoryMessageQueue queue) => _queue = queue;
public async Task PublishAsync<T>(
T integrationEvent,
CancellationToken cancellationToken = default)
where T : class, IIntegrationEvent
{
await _queue.Writer.WriteAsync(integrationEvent, cancellationToken);
}
}این سرویس:
- رویدادها را از طریق PublishAsync به کانال مینویسد.
- نیازی به تزریق وابستگیهای پیچیده یا دکوراتورها (Decorators) ندارد.
- کاملاً شفاف و قابلتنظیم است.
3. مصرف رویدادها (Consuming Events)
برای پردازش رویدادها، میتوان یک سرویس پسزمینه (Background Service) ایجاد کرد که از کانال بخواند و رویدادها را به هندلرهای مربوطه ارسال کند. مثال زیر یک مصرفکننده ساده را نشان میدهد:
using System.Threading.Channels;
using System.Threading.Tasks;
var channel = Channel.CreateBounded<int>(3);
var writer = channel.Writer;
var reader = channel.Reader;
Task.Run(async () =>
{
for (int i = 1; i <= 5; i++)
{
await writer.WriteAsync(i);
Console.WriteLine($"Sent: {i}");
await Task.Delay(500);
}
writer.Complete();
});
Task.Run(async () =>
{
while (await reader.WaitToReadAsync())
{
while (reader.TryRead(out int value))
{
Console.WriteLine($"Received: {value}");
}
}
});
await Task.Delay(3000);در این کد:
- یک کانال محدود با ظرفیت 3 ایجاد میشود.
- تولیدکننده (Producer) اعداد 1 تا 5 را با تأخیر 500 میلیثانیه ارسال میکند.
- مصرفکننده (Consumer) بهصورت ناهمگام دادهها را میخواند و نمایش میدهد.
- متد writer.Complete() پایان نوشتن را اعلام میکند.
این مثال نشان میدهد که چگونه کانالها میتوانند جریان دادهها را بهصورت ناهمگام و با کنترل فشار بازگشتی مدیریت کنند.
مزایای استفاده از System.Threading.Channels
استفاده از System.Threading.Channels به جای MediatR مزایای متعددی دارد:
- بدون وابستگی خارجی: کاهش ریسکهای مرتبط با بهروزرسانی بستهها و مشکلات مجوز.
- شفافیت کامل: کد کاملاً قابلفهم و بدون استفاده از بازتاب است.
- عملکرد بالا: طراحی شده برای سناریوهای با توان عملیاتی بالا، مشابه آنچه در ASP.NET Core استفاده میشود.
- انعطافپذیری: امکان افزودن قابلیتهایی مانند لاگگیری، دستهبندی (Batching) یا محدودسازی (Throttling) بهصورت دستی.
- آیندهنگری: با تکیه بر ابزارهای بومی، معماری برنامه در برابر تغییرات کتابخانههای خارجی مقاوم است.
چالشها و ملاحظات
هرچند System.Threading.Channels ساده و قدرتمند است، اما ممکن است برای سناریوهای پیچیدهتر، مانند مدیریت هندلرهای پویا یا رفتارهای تزئینی (Decorative Behaviors)، نیاز به کد بیشتری داشته باشد. همچنین، توسعهدهندگان باید مراقب دامهایی مانند فراموش کردن فراخوانی writer.Complete باشند، زیرا ممکن است باعث توقف مصرفکننده شود.
بهینهسازی عملکرد
برای بهبود عملکرد کانالها، میتوان از تنظیماتی مانند BoundedChannelOptions استفاده کرد. مثال زیر یک کانال محدود با حالت انتظار (Wait) را نشان میدهد:
var options = new BoundedChannelOptions(3) { FullMode = BoundedChannelFullMode.Wait };
var channel = Channel.CreateBounded<int>(options);
while (await reader.WaitToReadAsync())
{
var list = new List<int>();
while (reader.TryRead(out int value)) list.Add(value);
Console.WriteLine($"Batch: {string.Join(", ", list)}");
}این تنظیمات با کاهش سربار و خواندن دستهای دادهها، عملکرد را بهبود میبخشد.
کاربرد واقعی: رفع انسداد لاگها
در یک برنامه وب با حجم بالای لاگ، استفاده از یک کانال محدود با ظرفیت 1000، از انسداد صف جلوگیری کرد. این کانال امکان استریم لاگها را بهصورت ناهمگام فراهم کرد و سرعت پردازش را دو برابر نمود، در حالی که از خرابیهای ناشی از بار زیاد جلوگیری شد.
نتیجهگیری
System.Threading.Channels جایگزینی قدرتمند و بومی برای MediatR در برنامههای .NET است که سادگی، شفافیت و عملکرد بالا را به ارمغان میآورد. با استفاده از این ابزار، توسعهدهندگان میتوانند سیستمهای پیامرسانی سبک و قابلتنظیم ایجاد کنند که بدون وابستگی خارجی، نیازهای پروژههای مدرن را برآورده میکنند. اگرچه ممکن است برای سناریوهای پیچیده نیاز به کد بیشتری باشد، اما کنترل کامل بر معماری و انعطافپذیری آن، ارزش این تلاش را دارد.
توسعهدهندگان .NET میتوانند با آزمایش System.Threading.Channels در پروژههای خود، نهتنها وابستگی به کتابخانههای خارجی را کاهش دهند، بلکه معماری مقیاسپذیرتر و شفافتری ایجاد کنند. سادگی، همانطور که این ابزار نشان میدهد، کلید مقیاسپذیری است.