عنوان:

‫بررسی الگوی SAGA در میکروسرویس‌ها: رویکردهای Choreography و Orchestration و استفاده از MassTransit Automatonymous


نویسنده: محمد شریفی
تاریخ: ۱۴۰۳/۱۰/۱۴ ۰۰:۲۵
آدرس: www.dntips.ir
الگوی SAGA یک الگو برای مدیریت فرایندها و خطاها در سرویس‌های توزیع‌شده است. این الگو امکان هماهنگی تراکنش‌ها بین چندین سرویس را فراهم می‌کند تا در فرایندهای بلندمدت کسب‌وکار، انسجام حفظ شود. SAGA جریان رویدادها را ساده می‌کند و نقطه مرکزی حقیقت برای یک جریان کاری خاص است. SAGA باید بداند که پیام به کدام فرایند تعلق دارد. بنابراین، باید واضح باشد که چگونه یک پیام به فرایند خاصی مرتبط می‌شود که این کار با استفاده از CorrelationId انجام می‌شود. در این مقاله، برخی از رویکردهای الگوی SAGA، الگوی Request/Response، و کتابخانه MassTransit Automatonymous با نمونه‌هایی به تفصیل بررسی خواهیم کرد.

رویکردهای پیاده‌سازی الگوی SAGA
برخی رویکردها برای پیاده‌سازی الگوی SAGA وجود دارد که بسته به نیاز، می‌توان یکی را ترجیح داد. در این بخش، رویکردهای مبتنی بر Choreography و Orchestration بررسی خواهند شد.

1) SAGA مبتنی بر Choreography
در این رویکرد، تمام سرویس‌هایی که بخشی از فرایند کسب‌وکار ما هستند، هر بار که تراکنش محلی خود را تکمیل می‌کنند، با انتشار یک رویداد، سرویس بعدی را تحریک می‌کنند. بنابراین، نیازی به نقطه مرکزی مانند ارکستر یا ماشین وضعیت نیست. از طرفی، مشکل این رویکرد این است که ارتباطات نقطه به نقطه زیاد بین میکروسرویس‌ها منجر به پیچیدگی و وابستگی دایره‌ای می‌شود. به همین دلیل، این رویکرد می‌تواند زمانی مفید باشد که کمتر از ۳ یا ۴ سرویس در فرایند کسب‌وکار وجود داشته باشد و نیازی به نقطه مرکزی برای حفظ وضعیت فرایند نباشد.

2) SAGA مبتنی بر Orchestration
در این رویکرد، یک نقطه مرکزی به نام Saga State Machine وجود دارد. این ماشین تمامی تراکنش‌ها بین سرویس‌ها را مدیریت می‌کند و بر اساس رویدادها تصمیم می‌گیرد که کدام عملیات باید انجام شود. در طول فرایند بلندمدت کسب‌وکار، اگر مشکلی پیش بیاید، ماشین وضعیت رویدادهای جبران‌کننده برای بازگرداندن وضعیت قبل از گام شکست‌خورده منتشر می‌کند. همچنین، ماشین وضعیت داده‌هایی در مورد وضعیت ذخیره می‌کند تا جریان فرایند را کنترل کند و بر اساس وضعیت فعلی، فرایند را اجرا کند.

الگوی Request/Response
اولین نکته این است که باید نحوه عملکرد معمول پیام‌رسانی ناهمزمان را درک کنیم. برای مثال، زمانی که یک رویداد را به صورت "fire and forget" منتشر می‌کنیم، ناشر نمی‌داند که آیا رویداد مصرف شده است یا خیر. در این حالت، الگوی Request/Response به ما این امکان را می‌دهد که یک رویداد را منتشر کنیم، منتظر بمانیم تا رویداد مصرف شود و پاسخی دریافت کنیم بدون اینکه از Connection Pooling یا WebSocket استفاده کنیم.

دومین نکته این است که این الگو به ما امکان می‌دهد میکروسرویس‌ها را از هم جدا کنیم. معمولاً زمانی که ما یک سرویس دیگر را در حین فرایند کسب‌وکار فراخوانی می‌کنیم، درخواست HTTP همزمان ارسال می‌کنیم تا داده‌های مورد نیاز برای انجام مراحل بعدی را دریافت کنیم. با این الگو، ما سرویس‌ها را با یک درخواست HTTP فراخوانی نمی‌کنیم، بلکه رویداد را منتشر می‌کنیم و منتظر می‌مانیم تا رویداد مصرف شود و پاسخ آن را دریافت کنیم تا برای مراحل بعدی استفاده کنیم. این کار از فراخوانی سرویس‌ها با درخواست‌های HTTP همزمان جلوگیری می‌کند.

ماشین وضعیت MassTransit Automatonymous
MassTransit دارای یک کتابخانه ماشین وضعیت به نام Automatonymous است که شامل وضعیت‌ها، رویدادها و رفتارها می‌باشد. این کتابخانه کلاسی به نام MassTransitStateMachine دارد که می‌توان از آن برای تعریف ماشین وضعیت استفاده کرد و یک اینترفیس به نام SagaStateMachineInstance برای تعریف شیء مربوط به وضعیت فعلی و جزئیات فرایند مربوطه دارد. یک نمونه SAGA شیء‌ای است که می‌تواند از یک وضعیت به وضعیت دیگر انتقال یابد.

فرض کنید سیستمی داریم که شامل سرویس های زیر است:
1) شروع سفارش (Order Initialization): فرآیند با شروع سفارش آغاز می‌شود. در این مرحله، اطلاعات اولیه سفارش ثبت و آماده می‌شود تا زمینه برای مراحل بعدی فراهم گردد.
2) بررسی موجودی محصول (Check Product Stock): در این مرحله اطمینان حاصل می‌شود که محصولات مورد نیاز در انبار موجود هستند. سرویسی سطح موجودی را بررسی می‌کند و تنها در صورتی که موجودی کافی باشد، فرآیند ادامه می‌یابد.
3) پرداخت (Take Payment): پس از تایید موجودی، گام بعدی شامل انجام فرآیند پرداخت است. این ممکن است شامل دریافت مبلغ از روش پرداخت مشتری و تایید تراکنش باشد.
4) ایجاد سفارش (Create Order): در نهایت، پس از موفقیت‌آمیز بودن بررسی موجودی و پرداخت، سفارش ایجاد شده و به عنوان یک تراکنش تایید شده در سیستم ثبت می‌شود.

هر مرحله به گونه‌ای مدیریت می‌شود که با الگوی SAGA هماهنگ باشد، انسجام بین سرویس‌ها تضمین گردد و در صورت وقوع خطا، اقدمات جبرانی (compensating actions) مناسب انجام شود.

ابتدا دو کلاس OrderState و OrderStateMachine ایجاد میکنیم:
// Implementing the “ISagaVersion” interface is required to persist the data in MongoDB.
public class OrderState : SagaStateMachineInstance,ISagaVersion
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }
    public DateTime OrderStartDate { get; set; }
    public int Version { get; set; } 
}

در کلاس OrderStateMachine، ویژگی‌های مربوط به رویدادها (Event) را تعریف می‌کنیم. هر رویداد همچنین دارای یک رویداد جبرانی یا خطا (Compensation/Fault Event) است که عملیاتی که در آن رویداد اجرا شده را لغو می‌کند.
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
    
    }

    #region Events

    public Event<OrderProcessInitializationEvent> OrderProcessInitializationEvent { get; }
    public Event<Fault<OrderProcessInitializationEvent>> OrderProcessInitializationFaultEvent { get; }

    public Event<CheckProductStockEvent> CheckProductStockEvent { get; }
    public Event<Fault<CheckProductStockEvent>> CheckProductStockFaultEvent { get; }

    public Event<TakePaymentEvent> TakePaymentEvent { get; }
    public Event<Fault<TakePaymentEvent>> TakePaymentEventFaultEvent { get; }

    public Event<CreateOrderEvent> CreateOrderEvent { get; }
    public Event<Fault<CreateOrderEvent>> CreateOrderFaultEvent { get; }

    public Event<OrderProcessFailedEvent> OrderProcessFailedEvent { get; }

    #endregion
}

در این مرحله، ویژگی‌های مربوط به وضعیت‌ها (State Properties) را که به رویدادهای تعریف‌شده مرتبط هستند، در کلاس OrderStateMachine تعریف می‌کنیم.
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
    }

    #region States

    public State OrderProcessInitializedState { get; }
    public State OrderProcessInitializedFaultedState { get; }

    public State CheckProductStockState { get; }
    public State CheckProductStockFaultedState { get; }

    public State TakePaymentState { get; }
    public State TakePaymentFaultedState { get; }

    public State CreateOrderState { get; }
    public State CreateOrderFaultedState { get; }

    public State OrderProcessFailedState { get; }

    #endregion
}

سپس باید ویژگی‌ای را برای نگهداری وضعیت نمونه (Instance State) تعریف کنیم.
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState);
    }
}

پس از تعریف ویژگی‌ها در کلاس OrderStateMachine، باید این رویدادها (Events) را به ماشین حالت (State Machine) معرفی کنیم.
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        #region EventsDefinitions

        Event(() => OrderProcessInitializationEvent);
        Event(() => OrderProcessInitializationFaultEvent,
            x => x.CorrelateById(context => context.InitiatorId ?? context.Message.Message.OrderId));

        Event(() => CheckProductStockEvent);
        Event(() => CheckProductStockFaultEvent,
            x => x.CorrelateById(context => context.InitiatorId ?? context.Message.Message.OrderId));

        Event(() => TakePaymentEvent);
        Event(() => TakePaymentEventFaultEvent,
            x => x.CorrelateById(context => context.InitiatorId ?? context.Message.Message.OrderId));

        Event(() => CreateOrderEvent);
        Event(() => CreateOrderFaultEvent,
            x => x.CorrelateById(context => context.InitiatorId ?? context.Message.Message.OrderId));

        Event(() => OrderProcessFailedEvent);

        #endregion
    }
}

قطعه کد فوق مربوط به پیاده‌سازی یک ماشین حالت (State Machine) برای پردازش سفارش‌ها با استفاده از MassTransit و State Machine است. در کلاس فوق، یک سری رویدادهای مختلف برای ماشین حالت تعریف کرده ایم که جریان پردازش سفارش را مدیریت می‌کنند. در ادامه، بخش‌های مختلف کلاس فوق را توضیح خواهیم داد:

توضیحات:
1) <MassTransitStateMachine<OrderState: این کلاس از MassTransit است که برای پیاده‌سازی ماشین‌های حالت استفاده می‌شود. OrderState نوع حالت (state) است که ماشین وضعیت بر اساس آن طراحی شده است.
2) Event(): این متد برای تعریف رویدادها (Events) استفاده می‌شود. هر رویداد نمایانگر یک تغییر در وضعیت سفارش است که ماشین حالت باید به آن واکنش نشان دهد:
  • OrderProcessInitializationEvent: این رویداد برای شروع فرآیند پردازش سفارش تعریف شده است.
  • OrderProcessInitializationFaultEvent: این رویداد زمانی که خطایی در فرآیند پردازش سفارش اتفاق می‌افتد، فعال می‌شود. همچنین از CorrelateById برای مطابقت پیام‌های ورودی با یک شناسه خاص استفاده می‌شود که از InitiatorId یا OrderId پیام استخراج می‌شود.
  • CheckProductStockEvent: این رویداد زمانی فعال می‌شود که نیاز به بررسی موجودی محصولات باشد.
  • CheckProductStockFaultEvent: این رویداد زمانی فعال می‌شود که در حین بررسی موجودی محصولات خطایی رخ دهد.
  • TakePaymentEvent: این رویداد برای انجام عملیات پرداخت سفارش است.
  • TakePaymentEventFaultEvent: این رویداد زمانی که در حین پردازش پرداخت خطا رخ می‌دهد فعال می‌شود.
  • CreateOrderEvent: این رویداد زمانی که می‌خواهیم سفارش را ایجاد کنیم، فعال می‌شود.
  • CreateOrderFaultEvent: این رویداد زمانی که خطا در ایجاد سفارش رخ می‌دهد، فعال می‌شود.
  • OrderProcessFailedEvent: این رویداد برای مدیریت خطاهای کلی در فرآیند پردازش سفارش است.
  • مفهوم CorrelateById: در بیشتر رویدادها از متد CorrelateById استفاده شده است تا اطمینان حاصل شود که رویدادهای مختلف به یک سفارش خاص مرتبط هستند. این کار با استفاده از شناسه‌های InitiatorId یا OrderId انجام می‌شود. این شناسه‌ها برای ارتباط دادن پیام‌ها و رویدادها در طول ماشین وضعیت به کار می‌روند.

در این مرحله، باید یک جریان (Flow) بر اساس فرآیند کسب‌وکار خود طراحی کنیم:
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        #region Flow

        During(Initial,
            When(OrderProcessInitializationEvent)
                .Then(x => x.Saga.OrderStartDate = DateTime.Now)
                .TransitionTo(OrderProcessInitializedState));

        During(OrderProcessInitializedState,
            When(CheckProductStockEvent)
                .TransitionTo(CheckProductStockState));

        During(CheckProductStockState,
            When(TakePaymentEvent)
                .TransitionTo(TakePaymentState));

        During(TakePaymentState,
            When(CreateOrderEvent)
                .TransitionTo(CreateOrderState));

        #endregion

    }
}

توضیحات:
1) During: این متد برای تعیین وضعیت‌های مختلف ماشین حالت استفاده می‌شود و نشان‌دهنده فرآیندهای مختلفی است که ماشین حالت در طول آن وضعیت‌ها انجام می‌دهد.
در اینجا، برای هر وضعیت یک یا چند رویداد (Event) که ماشین وضعیت باید به آنها پاسخ دهد، تعریف شده است.

2) When: این متد برای تعریف رویدادهایی است که در هنگام وقوع آنها باید تغییرات در وضعیت اتفاق بیفتد.
برای هر رویداد، می‌توانید اقداماتی مانند تنظیم داده‌ها یا تغییر وضعیت انجام دهید.

در ادامه به جزئیات آن می پردازیم:
1. شروع پردازش:
During(Initial,
    When(OrderProcessInitializationEvent)
        .Then(x => x.Saga.OrderStartDate = DateTime.Now)
        .TransitionTo(OrderProcessInitializedState));
  • Initial: وضعیت اولیه ماشین است که معمولاً وقتی سفارش تازه ایجاد می‌شود شروع می‌شود.
  • When(OrderProcessInitializationEvent): وقتی رویداد OrderProcessInitializationEvent رخ می‌دهد، ماشین حالت باید تغییرات زیر را انجام دهد:
  • Then(x => x.Saga.OrderStartDate = DateTime.Now): این خط تاریخ و زمان شروع پردازش سفارش را در OrderStartDate ذخیره می‌کند.
  • TransitionTo(OrderProcessInitializedState): پس از انجام این کار، وضعیت ماشین به OrderProcessInitializedState تغییر می‌کند.

2. وضعیت پردازش سفارش اولیه:
During(OrderProcessInitializedState,
    When(CheckProductStockEvent)
        .TransitionTo(CheckProductStockState));
  • OrderProcessInitializedState: وضعیت پس از راه‌اندازی پردازش سفارش است.
  • When(CheckProductStockEvent): زمانی که رویداد CheckProductStockEvent رخ دهد، ماشین وضعیت به CheckProductStockState تغییر می‌کند.

3. وضعیت بررسی موجودی محصول:
During(CheckProductStockState,
    When(TakePaymentEvent)
        .TransitionTo(TakePaymentState));
  • CheckProductStockState: وضعیت‌ای است که در آن موجودی محصولات بررسی می‌شود.
  • When(TakePaymentEvent): زمانی که رویداد TakePaymentEvent رخ دهد، وضعیت ماشین به TakePaymentState تغییر می‌کند.

4. وضعیت پردازش پرداخت:
During(TakePaymentState,
    When(CreateOrderEvent)
        .TransitionTo(CreateOrderState));
  • TakePaymentState: وضعیت‌ای است که در آن پردازش پرداخت انجام می‌شود.
  • When(CreateOrderEvent): زمانی که رویداد CreateOrderEvent رخ دهد، ماشین وضعیت به CreateOrderState تغییر می‌کند.

در ادامه یک جریان جبرانی یا مدیریت خطا (Compensation/Fault Flow) ایجاد می کنیم تا عملیاتی که تا نقطه شکست انجام شده است را لغو کنیم.
using MassTransit;
using StateMachineSample.Events;

namespace StateMachineSample.StateMachine;

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        #region Fault-Companse State

        DuringAny(When(CreateOrderFaultEvent)
            .TransitionTo(CreateOrderFaultedState)
            .Then(context => context.Publish<Fault<TakePaymentEvent>>(new {context.Message})));


        DuringAny(When(TakePaymentEventFaultEvent)
            .TransitionTo(TakePaymentFaultedState)
            .Then(context => context.Publish<Fault<CheckProductStockEvent>>(new {context.Message})));

        DuringAny(When(CheckProductStockFaultEvent)
            .TransitionTo(CheckProductStockFaultedState)
            .Then(context => context.Publish<Fault<OrderProcessInitializationEvent>>(new {context.Message})));

        DuringAny(When(OrderProcessInitializationFaultEvent)
            .TransitionTo(OrderProcessInitializedFaultedState)
            .Then(context => context.Publish<OrderProcessFailedEvent>(new {OrderId = context.Saga.CorrelationId})));

        DuringAny(When(OrderProcessFailedEvent)
            .TransitionTo(OrderProcessFailedState));

        #endregion
    }
}

توضیحات:
1) ماشین وضعیت OrderStateMachine: این کد یک ماشین وضعیت (State Machine) به نام OrderStateMachine تعریف می‌کند که از کلاس <MassTransitStateMachine<OrderState ارث‌بری می‌کند. این ماشین وضعیت به منظور مدیریت و کنترل فرآیندهای مختلف مربوط به سفارش‌ها استفاده می‌شود.

2) انتقال وضعیت (Transitions): در این بخش، وضعیت‌های مختلف برای فرآیندهایی که ممکن است خطا داشته باشند، تعریف شده است. از DuringAny برای شنیدن رویدادهایی که در هر مرحله از ماشین وضعیت رخ می‌دهند و انجام انتقال به وضعیت‌های مختلف استفاده شده است.
  • CreateOrderFaultEvent: زمانی که خطا در ایجاد سفارش رخ می‌دهد، وضعیت به CreateOrderFaultedState تغییر می‌کند و سپس یک رویداد Fault<TakePaymentEvent> ارسال می‌شود.
  • TakePaymentEventFaultEvent: زمانی که خطا در پرداخت رخ می‌دهد، وضعیت به TakePaymentFaultedState تغییر می‌کند و سپس یک رویداد Fault<CheckProductStockEvent> ارسال می‌شود.
  • CheckProductStockFaultEvent: زمانی که خطا در بررسی موجودی محصول رخ می‌دهد، وضعیت به CheckProductStockFaultedState تغییر می‌کند و سپس یک رویداد Fault<OrderProcessInitializationEvent> ارسال می‌شود.
  • OrderProcessInitializationFaultEvent: زمانی که خطا در فرآیند اولیه سفارش رخ می‌دهد، وضعیت به OrderProcessInitializedFaultedState تغییر می‌کند و سپس یک رویداد OrderProcessFailedEvent ارسال می‌شود که شامل OrderId است.
  • OrderProcessFailedEvent: زمانی که فرآیند سفارش به شکست می‌انجامد، وضعیت به OrderProcessFailedState تغییر می‌کند.

3) انتشار رویدادها (Publishing Events): در هر مرحله از ماشین وضعیت، رویدادهای مختلفی به سیستم منتشر می‌شود. این رویدادها می‌توانند به دیگر بخش‌های سیستم اطلاع دهند که یک رویداد خاص (مانند خطا) رخ داده است.

به عنوان مثال، هنگامی که یک خطا در ایجاد سفارش رخ می‌دهد، یک رویداد <Fault<TakePaymentEvent منتشر می‌شود. این رویداد می‌تواند به بخش‌های دیگر سیستم اطلاع دهد که مشکلی در فرآیند پرداخت وجود دارد.
4) context.Saga.CorrelationId: در بخش‌هایی که از OrderProcessFailedEvent استفاده می‌شود، شناسه همبستگی (CorrelationId) از context.Saga استخراج شده و به عنوان OrderId به رویداد منتقل می‌شود. این شناسه برای پیگیری و شناسایی سفارش خاص استفاده می‌شود.

پیاده‌سازی OrderProcessInitializationEvent
using System.Runtime.CompilerServices;
using MassTransit;

namespace StateMachineSample.Events;

public class OrderProcessInitializationEvent
{
    public Guid OrderId { get; set; }
    
    [ModuleInitializer]
    internal static  void Init()
    {
        GlobalTopology.Send.UseCorrelationId<OrderProcessInitializationEvent>(x=> x.OrderId);
    }
}

از ایونت OrderProcessInitializationEvent برای ارسال یا دریافت پیام‌ها استفاده خواهیم کرد. در ادامه به جزئیات پیاده سازی آن می‌پردازیم:

توضیحات:
1. کلاس OrderProcessInitializationEvent
  • این کلاس نماینده یک رویداد (event) است که در یک سیستم مبتنی بر state machine یا معماری event-driven ارسال شود. این کلاس دارای پراپرتی OrderId است که یک شناسه (UUID) است که به طور منحصر به فرد، هر سفارش (Order) را مشخص می‌کند.

2. ویژگی ModuleInitializer
  • این ویژگی (attribute) در فضای نام System.Runtime.CompilerServices معرفی شده و به شما اجازه می‌دهد کدی را که باید قبل از استفاده از کلاس یا ماژول اجرا شود، مشخص کنید.
  • متد Init که با ویژگی ModuleInitializer تزئین شده است، به صورت خودکار هنگام بارگذاری ماژول فراخوانی می‌شود.

3. GlobalTopology.Send.UseCorrelationId
  • این خط از کد در متد Init استفاده شده و مربوط به تنظیمات MassTransit برای استفاده از یک CorrelationId است.
  • CorrelationId به MassTransit کمک می‌کند تا پیام‌ها را با یک شناسه مشترک مرتبط کند. در اینجا، پیام‌هایی که از نوع OrderProcessInitializationEvent هستند، بر اساس مقدار OrderId به هم مرتبط خواهند شد.

پیکربندی State Machine در فایل Program.cs
using MassTransit;
using StateMachineSample.StateMachine;
using StateMachineSample.API.Consumers;
using StateMachineSample.StateMachine.Settings;
using StateMachineSample.API.Settings;

var builder = WebApplication.CreateBuilder(args);

var messageBrokerQueueSettings = builder.Configuration.GetSection("MessageBroker:QueueSettings").Get<MessageBrokerQueueSettings>();
var messageBrokerPersistenceSettings = builder.Configuration.GetSection("MessageBroker:StateMachinePersistence").Get<MessageBrokerPersistenceSettings>();

builder.Services.AddMassTransit(x =>
{
    x.AddSagaStateMachine<OrderStateMachine, OrderState>().MongoDbRepository(r =>
    {
        r.Connection = messageBrokerPersistenceSettings.Connection;
        r.DatabaseName = messageBrokerPersistenceSettings.DatabaseName;
        r.CollectionName = messageBrokerPersistenceSettings.CollectionName;
    });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(messageBrokerQueueSettings.HostName, messageBrokerQueueSettings.VirtualHost, h =>
        {
            h.Username(messageBrokerQueueSettings.UserName);
            h.Password(messageBrokerQueueSettings.Password);
        });

        cfg.ConfigureEndpoints(context);
    });

    x.AddConsumer<OrderProcessInitializationEventConsumer>();
    x.AddRequestClient<OrderProcessInitializationEventConsumer>();

    x.AddConsumer<OrderProcessInitializationFaultEventConsumer>();
    x.AddRequestClient<OrderProcessInitializationFaultEventConsumer>();

    x.AddConsumer<CheckProductStockEventConsumer>();
    x.AddRequestClient<CheckProductStockEventConsumer>();

    x.AddConsumer<CheckProductStockFaultEventConsumer>();
    x.AddRequestClient<CheckProductStockFaultEventConsumer>();

    x.AddConsumer<TakePaymentEventConsumer>();
    x.AddRequestClient<TakePaymentEventConsumer>();

    x.AddConsumer<TakePaymentFaultEventConsumer>();
    x.AddRequestClient<TakePaymentFaultEventConsumer>();

    x.AddConsumer<CreateOrderEventConsumer>();
    x.AddRequestClient<CreateOrderEventConsumer>();

    x.AddConsumer<CreateOrderFaultEventConsumer>();
    x.AddRequestClient<CreateOrderFaultEventConsumer>();

    x.AddConsumer<OrderProcessFailedEventConsumer>();
    x.AddRequestClient<OrderProcessFailedEventConsumer>();
});

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

توضیحات:
  • پیکربندی‌های MessageBrokerQueueSettings و MessageBrokerPersistenceSettings از فایل‌های پیکربندی خوانده می‌شوند و برای اتصال به Message Broker و State Machine Persistence استفاده می‌شوند.
  • سپس، MassTransit پیکربندی می‌شود.
  • <AddSagaStateMachine<OrderStateMachine, OrderState(): این کد یک State Machine برای مدیریت وضعیت‌ها و تغییرات موجودیت OrderStateMachine (که نوعی از SagaStateMachine است) ثبت می‌کند.
  • در بخش MongoDbRepository(r => { ... })، اطلاعات مربوط به MongoDB برای ذخیره وضعیت‌های State Machine تنظیم می‌شود (اتصال به پایگاه داده، نام پایگاه داده و مجموعه مربوط به وضعیت‌ها).
  • UsingRabbitMq: این بخش، پیکربندی مربوط به RabbitMQ (Message Broker) را انجام می‌دهد.
  • cfg.Host: آدرس سرور RabbitMQ و اطلاعات مربوط به اتصال مانند نام کاربری و رمز عبور را تنظیم می‌کند.
  • cfg.ConfigureEndpoints: این بخش به MassTransit می‌گوید که نقاط پایانی RabbitMQ را پیکربندی کند.
  • Consumers: در این بخش، چندین Consumer به MassTransit اضافه می‌شود که مسئولیت دریافت رویدادها (Events) و درخواست‌های مختلف مانند شروع پردازش سفارش، بررسی موجودی کالا، انجام پرداخت و غیره را بر عهده دارند.
  • RequestClient: برای هر مصرف‌کننده، RequestClient مربوطه نیز اضافه می‌شود که به سیستم امکان ارسال درخواست‌های همزمان به سایر سرویس‌ها را می‌دهد.

یک کنترلر به نام EventsController تعریف می کنیم که از MassTransit برای ارسال رویدادها به سرویس‌های دیگر استفاده می‌کند.
using MassTransit;
using Microsoft.AspNetCore.Mvc;
using StateMachineSample.API.Requests;
using StateMachineSample.Events;
using StateMachineSample.Events.Responses;

namespace StateMachineSample.API.Controllers;

public class EventsController
{
    private readonly IPublishEndpoint _publishEndpoint;
    private readonly IRequestClient<OrderProcessInitializationEvent> _orderProcessInitializationEventRequestClient;
    private readonly IRequestClient<CheckProductStockEvent> _checkProductStockEventRequestClient;
    private readonly IRequestClient<TakePaymentEvent> _takePaymentEventRequestClient;
    private readonly IRequestClient<CreateOrderEvent> _createOrderEventRequestClient;
        
    public EventsController(
        IPublishEndpoint publishEndpoint,
        IRequestClient<OrderProcessInitializationEvent> orderProcessInitializationEventRequestClient,
        IRequestClient<CheckProductStockEvent> checkProductStockEventRequestClient,
        IRequestClient<TakePaymentEvent> takePaymentEventRequestClient,
        IRequestClient<CreateOrderEvent> createOrderEventRequestClient)
    {
        _publishEndpoint = publishEndpoint;
        _orderProcessInitializationEventRequestClient = orderProcessInitializationEventRequestClient;
        _checkProductStockEventRequestClient = checkProductStockEventRequestClient;
        _takePaymentEventRequestClient = takePaymentEventRequestClient;
        _createOrderEventRequestClient = createOrderEventRequestClient;
    }
}

توضیحات:
  • برای استفاده از MassTransit در این کنترلر، از چندین وابستگی (dependency) استفاده شده است که به‌طور خاص برای ارسال و دریافت رویدادها و درخواست‌ها استفاده می شوند.
  • IPublishEndpoint: این وابستگی برای ارسال پیام‌ها به سیستم‌های دیگر استفاده می‌شود. به عبارتی، از این سرویس برای انتشار رویدادها به‌طور عمومی استفاده می‌شود تا سایر سیستم‌ها از آن اطلاع پیدا کنند.
  • <IRequestClient<T: این وابستگی برای ارسال درخواست‌ها به سیستم‌های دیگر و دریافت پاسخ استفاده می‌شود. در اینجا به‌طور خاص از آن برای ارسال رویدادهای مختلف مانند OrderProcessInitializationEvent، CheckProductStockEvent و غیره استفاده شده است. این روش به شما این امکان را می‌دهد که منتظر پاسخ سرویس مقصد باشید و بتوانید عملیات بعدی را انجام دهید.

نمونه‌ای از endpoint برای فعال‌سازی ایونت initialized:
در کنترلر EventsController، یک endpoint برای فعال‌سازی ایونت OrderProcessInitializedEvent ایجاد می کنیم
using MassTransit;
using Microsoft.AspNetCore.Mvc;
using StateMachineSample.API.Requests;
using StateMachineSample.Events;
using StateMachineSample.Events.Responses;

namespace StateMachineSample.API.Controllers;

public class EventsController
{

    [HttpPost("initialize/order")]
    public async Task<IActionResult> OrderProcessInitializedEvent([FromBody] EventCommonRequest request)
    {
        if (!request.IsRequestResponsePattern)
        {
            await _publishEndpoint.Publish<OrderProcessInitializationEvent>(new {request.OrderId});
            return new NoContentResult();
        }

        var result = await _orderProcessInitializationEventRequestClient.GetResponse<OrderProcessInitiazationDto>(new {request.OrderId});

        return new NoContentResult();
    }
}

در صورتی که IsRequestResponsePattern برابر با false باشد، تنها ایونت منتشر می‌شود و پاسخ فوری (NoContent) داده می‌شود.
اگر IsRequestResponsePattern برابر با true باشد، الگوی درخواست/پاسخ پیاده‌سازی می‌شود. در این حالت، ایونت منتشر شده و سپس منتظر دریافت پاسخ از مصرف‌کننده می‌ماند.

پیاده‌سازی الگوی Template Method
در این مقاله جهت مدیریت ایونت‌ها و مصرف‌کننده‌ها از الگوی "Template Method" استفاده می کنیم که برای کاهش تکرار کد، افزایش قابلیت توسعه و انعطاف‌پذیری بیشتر مورد استفاده قرار می گیرد. این الگو به طور خاص زمانی مفید است که شما بخواهید منطق عمومی یک فرآیند را در یک مکان مرکزی پیاده‌سازی کنید، در حالی که جزئیات خاص آن فرآیند را به کلاس‌های فرزند واگذار کنید.
ابتدا یک کلاس انتزاعی به نام <ConsumerBase<T تعریف می کنیم که از اینترفیس <IConsumer<T پیروی می‌کند. این کلاس پایه مسئول مدیریت اجرای عمومی ایونت‌ها و پردازش خطاها است، در حالی که منطق خاص هر ایونت در کلاس‌های فرزند پیاده‌سازی خواهد شد.
using MassTransit;

namespace StateMachineSample.API.Consumers.Base;

public abstract class ConsumerBase<T> : IConsumer<T> where T : class
{
    public async Task Consume(ConsumeContext<T> context)
    {
        try
        {
            await ConsumeInternal(context);
        }
        catch (Exception e)
        {
            await context.Publish<Fault<T>>(context);
            
            // global exception handling
            throw;
        }
    }

    protected abstract Task ConsumeInternal(ConsumeContext<T> context);
}

در اینجا:
  • متد Consume مسئول پردازش پیام‌ها است و در صورت بروز هر گونه استثنا، ایونت خطا (<Fault<T) منتشر می‌شود.
  • متد ConsumeInternal که انتزاعی است، در کلاس‌های فرزند پیاده‌سازی خواهد شد و منطق خاص برای هر نوع ایونت در آن قرار می‌گیرد.

کلاس‌های فرزند: پیاده‌سازی منطق خاص
در کلاس‌های فرزند، منطق خاص برای مصرف‌کننده‌ها پیاده‌سازی می‌شود. به عنوان مثال، یک مصرف‌کننده برای ایونت OrderProcessInitializationEvent:
using MassTransit;
using StateMachineSample.API.Consumers.Base;
using StateMachineSample.Events;
using StateMachineSample.Events.Responses;

namespace StateMachineSample.API.Consumers;

public class OrderProcessInitializationEventConsumer : ConsumerBase<OrderProcessInitializationEvent>
{
    protected override Task ConsumeInternal(ConsumeContext<OrderProcessInitializationEvent> context)
    {
        Console.WriteLine("Order process Initialized");

        context.RespondAsync(new OrderProcessInitiazationDto
        {
            OrderId = context.Message.OrderId
        });
        
        return Task.CompletedTask;
    }
}

در اینجا:
  • منطق خاص برای ایونت OrderProcessInitializationEvent در متد ConsumeInternal پیاده‌سازی شده است.
  • پس از پردازش ایونت، با استفاده از متد RespondAsync، یک شیء پاسخ به ناشر ارسال می‌شود (اگر از الگوی درخواست/پاسخ استفاده می‌کنیم).


در این مقاله، الگوی Saga و دو رویکرد آن (الگوی Saga مبتنی بر Choreography و Orchestration) توضیح داده شد. همچنین به پیام‌رسانی Async با ماشین حالت Saga، کتابخانه MassTransit Automatonymous و الگوی Request/Response پرداخته شد. الگوی Saga با پردازش سیستماتیک تراکنش‌های متعدد در سرویس‌های مختلف و مستقل، انسجام داده‌ها را حفظ می‌کند. برای این منظور، این الگو از دو رویکرد متفاوت برای دستیابی به هدف خود استفاده می‌کند، و انتخاب بهترین رویکرد از میان این گزینه‌ها بستگی به شرایط خاص هر مورد دارد.