聊聊MassTransit——狀態機實現Saga模式(譯)

翻譯自 Saga State Machines

Saga State Machines(狀態機)

Saga State Machines(狀態機)以前被稱爲Automatonymous,從v8開始被合併到masstrtransit代碼庫中。

介紹

Automatonymous是.Net的State Machines(狀態機)類庫,它提供了一種C#語法來定義State Machines,包括狀態、事件和行爲。MassTransit包括Automatonymous,並添加了實例存儲、事件關聯、消息綁定、請求和響應支持以及調度。

Automatonymous不再是一個獨立的NuGet包,它已經被MassTransit包含了。在以前的版本中,需要額外的包引用。所以之前如果引用了Automatonymous,則必須刪除該引用,因爲它不再兼容。

State Machine(狀態機)

State Machine(狀態機)定義狀態、事件和行爲。實現一個派生自MassTransitStateMachine<T>的狀態機類,該類只創建一次,然後用於將事件觸發的行爲應用於狀態機實例。

public class OrderStateMachine:MassTransitStateMachine<OrderState>
{}

Instance(實例)

Instance包含狀態機實例的數據。當沒有找到具有相同CorrelationId的現有實例時,將爲每個已消費的初始事件創建一個新實例。一個Saga Repository用於持久化實例。實例是類,並且必須實現SagaStateMachineInstance接口。

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState);
    }
}

Instance實例必須存儲當前狀態(CurrentState),它可以是以下三種類型之一:

類型 描述
State 接口狀態SagaStateMachineInstance。可能難以序列化,通常僅用於內存實例,但如果repository存儲引擎支持將用戶類型映射到存儲類型,則可以使用。
string State的名稱。但是,它佔用了大量空間,因爲每個實例都重複狀態名。
int 小,快,但要求指定每個可能的狀態,以便爲每個狀態分配int值。

如果CurrentState實例狀態屬性是state,則自動配置它。對於string或int類型,必須使用InstanceState方法。

要指定int狀態值,請配置instance實例狀態,如下所示。

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public int CurrentState { get; set; }
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState, Submitted, Accepted);
    }
}

結果如下值:

0 - None, 1 - Initial, 2 - Final, 3 - Submitted, 4 - Accepted

State(狀態)

States(狀態)表示事件(events)消費後實例的當前狀態。一個實例在給定時間只能處於一種狀態。新實例默認爲初始(Initial)狀態,這是自動定義的。還爲所有狀態機定義了最終(Final)狀態,並用於表示實例已達到最終狀態。

在這個例子中,聲明瞭兩個狀態(State)。狀態由MassTransitStateMachine基類構造函數自動初始化。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public State Submitted { get; private set; }
    public State Accepted { get; private set; }
}

Event(事件)

事件(Event)是可能導致狀態(State)變化的發生的事情。事件(Event)可以添加或更新實例數據,也可以更改實例(instance)的當前狀態。Event是泛型的,其中T必須是有效的消息類型。

在下面的示例中,SubmitOrder消息被聲明爲一個事件,包括如何將該事件與實例關聯。

除非事件實現了 CorrelatedBy,否則它們必須用關聯表達式聲明。

public interface SubmitOrder
{
    Guid OrderId { get; }    
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
    }
    public Event<SubmitOrder> SubmitOrder { get; private set; }
}

Behavior(行爲)

行爲是指在狀態(state)中發生事件(event)時所發生的情況。

下面,Initial塊用於定義在Initial狀態期間SubmitOrder事件的行爲。當使用SubmitOrder消息並且沒有找到具有與OrderId匹配的CorrelationId的實例時,將在Initial狀態下創建一個新實例。TransitionTo activity 將實例轉換到Submitted狀態,之後使用saga repository持久化實例。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted));
    }
}

隨後,OrderAccepted事件可以通過下面所示的行爲來處理。

public interface OrderAccepted
{
    Guid OrderId { get; }    
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId));
        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted));
    }
    public Event<OrderAccepted> OrderAccepted { get; private set; }
}
Message Order(消息順序)

Message brokers(MQ)通常不保證消息順序。因此,在狀態機(state machine)設計中考慮無序消息是很重要的。

在上面的示例中,在OrderAccepted事件之後接收SubmitOrder消息可能會導致SubmitOrder消息在_error隊列中結束。如果OrderAccepted事件首先被接收,它將被丟棄,因爲它在初始(Initial)狀態下不被接受。下面是處理這兩種場景的更新狀態機。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted),
            When(OrderAccepted)
                .TransitionTo(Accepted));
        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted));
        During(Accepted,
            Ignore(SubmitOrder));
    }
}

在更新後的示例中,在接受(Accepted)狀態下接收SubmitOrder消息會忽略該事件。然而,事件中的數據可能是有用的。在這種情況下,可以添加將數據複製到實例的行爲。下面,在兩個場景中捕獲事件的數據。

public interface SubmitOrder
{
    Guid OrderId { get; }

    DateTime OrderDate { get; }
}

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }

    public DateTime? OrderDate { get; set; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .Then(x => x.Saga.OrderDate = x.Message.OrderDate)
                .TransitionTo(Submitted),
            When(OrderAccepted)
                .TransitionTo(Accepted));

        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted));

        During(Accepted,
            When(SubmitOrder)
                .Then(x => x.Saga.OrderDate = x.Message.OrderDate));
    }
}

Configuration(配置)

配置saga state machine(狀態機)

services.AddMassTransit(x =>
{
    x.AddSagaStateMachine<OrderStateMachine, OrderState>()
        .InMemoryRepository();
});

上面的示例使用內存中的saga repository,但是可以使用任何saga repository。持久性部分包括受支持的saga repository的詳細信息。

要測試state machine(狀態機),請參閱測試部分。

Event(事件)

如上所示,事件(event)是狀態機(state machine)可以使用的消息。事件(event)可以指定任何有效的消息類型,並且可以配置每個事件。有幾種事件配置方法可用。

內置的CorrelatedBy<Guid>接口可以在消息約定中使用,以指定事件CorrelationId

public interface OrderCanceled :
    CorrelatedBy<Guid>
{    
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderCanceled); // not required, as it is the default convention
    }
}

雖然上面顯式聲明瞭事件(event),但這不是必需的。默認將會自動的配置爲CorrelatedBy<Guid>接口的事件(event)。

雖然方便,但有些人認爲接口是對消息契約基礎設施的入侵。MassTransit還支持一種聲明性方法來爲事件指定CorrelationId。通過配置全局消息拓撲,可以指定要用於關聯的消息屬性。

public interface SubmitOrder
{    
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    // this is shown here, but can be anywhere in the application as long as it executes
    // before the state machine instance is created. Startup, etc. is a good place for it.
    // It only needs to be called once per process.
    static OrderStateMachine()
    {
        GlobalTopology.Send.UseCorrelationId<SubmitOrder>(x => x.OrderId);
    }

    public OrderStateMachine()
    {
        Event(() => SubmitOrder);
    }

    public Event<SubmitOrder> SubmitOrder { get; private set; }
}

另一種方法是聲明事件相關性,如下所示。當上述兩種方法都未使用時,應使用此方法。

public interface SubmitOrder
{    
    Guid OrderId { get; }
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
    }
    public Event<SubmitOrder> SubmitOrder { get; private set; }
}

因爲OrderId是一個Guid,所以它可以用於事件關聯。當在初始狀態下接受SubmitOrder時,由於OrderId是Guid,因此新實例上的CorrelationId會自動分配OrderId值。
還可以使用查詢表達式關聯事件,當事件沒有與實例的CorrelationId屬性關聯時,需要使用查詢表達式。查詢的開銷更大,並且可能匹配多個實例,在設計狀態機和事件時應該考慮到這一點。

只要可能,嘗試使用CorrelationId進行關聯。如果需要查詢,則可能需要在屬性上創建索引,以便優化數據庫查詢。

要使用另一種類型關聯事件,需要額外的配置。

public interface ExternalOrderSubmitted
{    
    string OrderNumber { get; }
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => ExternalOrderSubmitted, e => e
            .CorrelateBy(i => i.OrderNumber, x => x.Message.OrderNumber)
            .SelectId(x => NewId.NextGuid()));
    }
    public Event<ExternalOrderSubmitted> ExternalOrderSubmitted { get; private set; }
}

還可以使用兩個參數編寫查詢,這兩個參數直接傳遞給repository(並且必須得到後臺數據庫的支持)。

public interface ExternalOrderSubmitted
{    
    string OrderNumber { get; }
}
public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => ExternalOrderSubmitted, e => e
            .CorrelateBy((instance,context) => instance.OrderNumber == context.Message.OrderNumber)
            .SelectId(x => NewId.NextGuid()));
    }
    public Event<ExternalOrderSubmitted> ExternalOrderSubmitted { get; private set; }
}

當事件沒有與實例唯一相關的Guid時,必須配置.selectid表達式。在上面的示例中,NewId用於生成一個順序標識符,該標識符將分配給實例CorrelationId。事件上的任何屬性都可以用來初始化CorrelationId。

Ignore Event(忽略事件)

可能有必要忽略給定狀態下的事件,以避免錯誤生成,或者防止消息被移動到_skip隊列。要忽略某個狀態中的事件,請使用ignore方法。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted),
            When(OrderAccepted)
                .TransitionTo(Accepted));

        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted));

        During(Accepted,
            Ignore(SubmitOrder));
    }
}

Composite Event(組合事件)

通過指定一個或多個必須使用的事件來配置組合事件,之後將引發組合事件。組合事件使用實例屬性來跟蹤所需的事件,這是在配置期間指定的。

要定義組合事件,必須首先配置所需的事件以及任何事件行爲,然後才能配置組合事件。

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }

    public int ReadyEventStatus { get; set; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted),
            When(OrderAccepted)
                .TransitionTo(Accepted));

        During(Submitted,
            When(OrderAccepted)
                .TransitionTo(Accepted));

        CompositeEvent(() => OrderReady, x => x.ReadyEventStatus, SubmitOrder, OrderAccepted);

        DuringAny(
            When(OrderReady)
                .Then(context => Console.WriteLine("Order Ready: {0}", context.Saga.CorrelationId)));
    }

    public Event OrderReady { get; private set; }
}

一旦使用了SubmitOrderOrderAccepted事件,就會觸發OrderReady事件。

Missing Instance

如果事件與實例不匹配,則可以配置缺失的實例行爲

public interface RequestOrderCancellation
{    
    Guid OrderId { get; }
}

public interface OrderNotFound
{
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderCancellationRequested, e =>
        {
            e.CorrelateById(context => context.Message.OrderId);

            e.OnMissingInstance(m =>
            {
                return m.ExecuteAsync(x => x.RespondAsync<OrderNotFound>(new { x.OrderId }));
            });
        });
    }

    public Event<RequestOrderCancellation> OrderCancellationRequested { get; private set; }
}

在本例中,當在沒有匹配實例的情況下使用取消訂單請求時,將發送未找到訂單的響應。響應更顯式,而不是生成Fault。其他缺少的實例選項包括DiscardFaultExecute (ExecuteAsync的同步版本)。

Initial Insert(初始化插入)

爲了提高新實例的性能,將事件配置爲直接插入到saga repository中可以減少鎖爭用。要配置要插入的事件,它應該位於initial塊中,並指定一個saga工廠。

public interface SubmitOrder
{    
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => SubmitOrder, e => 
        {
            e.CorrelateById(context => context.Message.OrderId));

            e.InsertOnInitial = true;
            e.SetSagaFactory(context => new OrderState
            {
                CorrelationId = context.Message.OrderId
            })
        });

        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted));
    }

    public Event<SubmitOrder> SubmitOrder { get; private set; }
}

在使用InsertOnInitial時,至關重要的是,saga repository能夠檢測重複的鍵(在本例中,是使用OrderId初始化的CorrelationId)。在這種情況下,在CorrelationId上使用集羣主鍵可以防止插入重複的實例。如果使用不同的屬性關聯事件,請確保數據庫對實例屬性實施唯一約束,並且saga工廠使用事件屬性值初始化實例屬性。

public interface ExternalOrderSubmitted
{    
    string OrderNumber { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => ExternalOrderSubmitted, e => 
        {
            e.CorrelateBy(i => i.OrderNumber, x => x.Message.OrderNumber)
            e.SelectId(x => NewId.NextGuid());

            e.InsertOnInitial = true;
            e.SetSagaFactory(context => new OrderState
            {
                CorrelationId = context.CorrelationId ?? NewId.NextGuid(),
                OrderNumber = context.Message.OrderNumber,
            })
        });

        Initially(
            When(SubmitOrder)
                .TransitionTo(Submitted));
    }

    public Event<ExternalOrderSubmitted> ExternalOrderSubmitted { get; private set; }
}

數據庫將對OrderNumber使用唯一約束來防止重複,saga repository將將其檢測爲現有實例,然後加載該實例以使用事件。

Completed Instance

默認情況下,實例不會從saga repository中刪除。若要配置已完成的實例刪除,請指定用於確定實例是否已完成的方法。

public interface OrderCompleted
{    
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId));

        DuringAny(
            When(OrderCompleted)
                .Finalize());

        ();
    }

    public Event<OrderCompleted> OrderCompleted { get; private set; }
}

當實例使用OrderCompleted事件時,實例將被完成(它將實例轉換爲Final狀態)。SetCompletedWhenFinalized方法將一個處於Final狀態的實例定義爲已完成——然後由saga repository使用它來刪除該實例。

要使用不同的完成表達式,例如檢查實例是否處於完成狀態的表達式,請使用SetCompleted方法,如下所示。

public interface OrderCompleted
{    
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId));

        DuringAny(
            When(OrderCompleted)
                .TransitionTo(Completed));

        SetCompleted(async instance => 
        {
            State<TInstance> currentState = await this.GetState(instance);

            return Completed.Equals(currentState);
        });
    }

    public State Completed { get; private set; }
    public Event<OrderCompleted> OrderCompleted { get; private set; }
}

Activities

狀態機行爲被定義爲響應事件而執行的一系列活動。除了automautonomous中包含的活動之外,MassTransit還包括用於發送、發佈和調度消息以及發起和響應請求的活動。

Publish

要發佈事件,請添加publish活動。

public interface OrderSubmitted
{
    Guid OrderId { get; }    
}

public class OrderSubmittedEvent :
    OrderSubmitted
{
    public OrderSubmittedEvent(Guid orderId)
    {
        OrderId = orderId;
    }

    public Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .Publish(context => (OrderSubmitted)new OrderSubmittedEvent(context.Saga.CorrelationId))
                .TransitionTo(Submitted));
    }
}

或者,可以使用消息初始化器來去除Event類。

public interface OrderSubmitted
{
    Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .PublishAsync(context => context.Init<OrderSubmitted>(new { OrderId = context.Saga.CorrelationId }))
                .TransitionTo(Submitted));
    }
}

Send

要發送消息,請添加send活動。

public interface UpdateAccountHistory
{
    Guid OrderId { get; }    
}

public class UpdateAccountHistoryCommand :
    UpdateAccountHistory
{
    public UpdateAccountHistoryCommand(Guid orderId)
    {
        OrderId = orderId;
    }

    public Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine(OrderStateMachineSettings settings)
    {
        Initially(
            When(SubmitOrder)
                .Send(settings.AccountServiceAddress, context => new UpdateAccountHistoryCommand(context.Saga.CorrelationId))
                .TransitionTo(Submitted));
    }
}

或者,可以使用消息初始化器來去除Command類。

public interface UpdateAccountHistory
{
    Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine(OrderStateMachineSettings settings)
    {
        Initially(
            When(SubmitOrder)
                .SendAsync(settings.AccountServiceAddress, context => context.Init<UpdateAccountHistory>(new { OrderId = context.Saga.CorrelationId }))
                .TransitionTo(Submitted));
    }
}

Respond

狀態機可以通過將請求消息類型配置爲事件,並使用response方法來響應請求。在配置請求事件時,建議配置缺失的實例方法,以提供更好的響應體驗(通過不同的響應類型,或者通過指示未找到實例的響應)。

public interface RequestOrderCancellation
{    
    Guid OrderId { get; }
}

public interface OrderCanceled
{
    Guid OrderId { get; }
}

public interface OrderNotFound
{
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Event(() => OrderCancellationRequested, e =>
        {
            e.CorrelateById(context => context.Message.OrderId);

            e.OnMissingInstance(m =>
            {
                return m.ExecuteAsync(x => x.RespondAsync<OrderNotFound>(new { x.OrderId }));
            });
        });

        DuringAny(
            When(OrderCancellationRequested)
                .RespondAsync(context => context.Init<OrderCanceled>(new { OrderId = context.Saga.CorrelationId }))
                .TransitionTo(Canceled));
    }

    public State Canceled { get; private set; }
    public Event<RequestOrderCancellation> OrderCancellationRequested { get; private set; }
}

有些場景需要等待狀態機的響應。在這些場景中,應該存儲響應原始請求所需的信息。

public record CreateOrder(Guid CorrelationId) : CorrelatedBy<Guid>;

public record ProcessOrder(Guid OrderId, Guid ProcessingId);

public record OrderProcessed(Guid OrderId, Guid ProcessingId);

public record OrderCancelled(Guid OrderId, string Reason);

public class ProcessOrderConsumer : IConsumer<ProcessOrder>
{
    public async Task Consume(ConsumeContext<ProcessOrder> context)
    {
        await context.RespondAsync(new OrderProcessed(context.Message.OrderId, context.Message.ProcessingId));
    }
}

public class OrderState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }
    public Guid? ProcessingId { get; set; }
    public Guid? RequestId { get; set; }
    public Uri ResponseAddress { get; set; }
    public Guid OrderId { get; set; }
}

public class OrderStateMachine : MassTransitStateMachine<OrderState>
{
    public State Created { get; set; }
    
    public State Cancelled { get; set; }
    
    public Event<CreateOrder> OrderSubmitted { get; set; }
    
    public Request<OrderState, ProcessOrder, OrderProcessed> ProcessOrder { get; set; }
    
    public OrderStateMachine()
    {
        InstanceState(m => m.CurrentState);
        Event(() => OrderSubmitted);
        Request(() => ProcessOrder, order => order.ProcessingId, config => { config.Timeout = TimeSpan.Zero; });

        Initially(
            When(OrderSubmitted)
                .Then(context =>
                {
                    context.Saga.CorrelationId = context.Message.CorrelationId;
                    context.Saga.ProcessingId = Guid.NewGuid();

                    context.Saga.OrderId = Guid.NewGuid();

                    context.Saga.RequestId = context.RequestId;
                    context.Saga.ResponseAddress = context.ResponseAddress;
                })
                .Request(ProcessOrder, context => new ProcessOrder(context.Saga.OrderId, context.Saga.ProcessingId!.Value))
                .TransitionTo(ProcessOrder.Pending));
        
        During(ProcessOrder.Pending,
            When(ProcessOrder.Completed)
                .TransitionTo(Created)
                .ThenAsync(async context =>
                {
                    var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress);
                    await endpoint.Send(context.Saga, r => r.RequestId = context.Saga.RequestId);
                }),
            When(ProcessOrder.Faulted)
                .TransitionTo(Cancelled)
                .ThenAsync(async context =>
                {
                    var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress);
                    await endpoint.Send(new OrderCancelled(context.Saga.OrderId, "Faulted"), r => r.RequestId = context.Saga.RequestId);
                }),
            When(ProcessOrder.TimeoutExpired)
                .TransitionTo(Cancelled)
                .ThenAsync(async context =>
                {
                    var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress);
                    await endpoint.Send(new OrderCancelled(context.Saga.OrderId, "Time-out"), r => r.RequestId = context.Saga.RequestId);
                }));
    }
}

Schedule

狀態機可以調度事件,它使用消息調度器來調度要傳遞給實例的消息。首先,必須聲明Schedule。

public interface OrderCompletionTimeoutExpired
{
    Guid OrderId { get; }
}

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }

    public Guid? OrderCompletionTimeoutTokenId { get; set; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Schedule(() => OrderCompletionTimeout, instance => instance.OrderCompletionTimeoutTokenId, s =>
        {
            s.Delay = TimeSpan.FromDays(30);

            s.Received = r => r.CorrelateById(context => context.Message.OrderId);
        });
    }

    public Schedule<OrderState, OrderCompletionTimeoutExpired> OrderCompletionTimeout { get; private set; }
}

配置指定了可以被調度活動覆蓋的Delay,以及Received事件的相關表達式。狀態機可以使用Received事件,如下所示。

OrderCompletionTimeoutTokenId是一個Guid?用於跟蹤計劃消息tokenId的實例屬性,稍後可使用該屬性取消對事件的計劃。

public interface OrderCompleted
{
    Guid OrderId { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        During(Accepted,
            When(OrderCompletionTimeout.Received)
                .PublishAsync(context => context.Init<OrderCompleted>(new { OrderId = context.Saga.CorrelationId }))
                .Finalize());
    }

    public Schedule<OrderState, OrderCompletionTimeoutExpired> OrderCompletionTimeout { get; private set; }
}

可以使用Schedule活動安排事件。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        During(Submitted,
            When(OrderAccepted)
                .Schedule(OrderCompletionTimeout, context => context.Init<OrderCompletionTimeoutExpired>(new { OrderId = context.Saga.CorrelationId }))
                .TransitionTo(Accepted));
    }
}

如上所述,可以通過Schedule活動覆蓋延遲。實例和消息(context.Data)內容都可以用來計算延遲。

public interface OrderAccepted
{
    Guid OrderId { get; }    
    TimeSpan CompletionTime { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        During(Submitted,
            When(OrderAccepted)
                .Schedule(OrderCompletionTimeout, context => context.Init<OrderCompletionTimeoutExpired>(new { OrderId = context.Saga.CorrelationId }),
                    context => context.Message.CompletionTime)
                .TransitionTo(Accepted));
    }
}

一旦收到預定的事件,就會清除OrderCompletionTimeoutTokenId屬性。

如果不再需要計劃的事件,則可以使用Unschedule活動。

public interface OrderAccepted
{
    Guid OrderId { get; }    
    TimeSpan CompletionTime { get; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        DuringAny(
            When(OrderCancellationRequested)
                .RespondAsync(context => context.Init<OrderCanceled>(new { OrderId = context.Saga.CorrelationId }))
                .Unschedule(OrderCompletionTimeout)
                .TransitionTo(Canceled));
    }
}

Request

狀態機可以使用request方法發送請求,該方法指定了請求類型和響應類型。可以指定其他請求設置,包括ServiceAddress和Timeout。

如果指定了ServiceAddress,它應該是將響應請求的服務的端點地址。如果沒有指定,請求將被髮布。

默認超時時間爲30秒,但任何大於或等於TimeSpan.Zero的值都可以。當發送的請求超時大於零時,將調度TimeoutExpired消息。指定TimeSpan.Zero 不會調度超時消息,並且請求永遠不會超時。

在定義請求時,應該指定一個實例屬性來存儲用於將響應與狀態機實例相關聯的RequestId。當請求掛起時,RequestId存儲在屬性中。當請求完成後,該屬性被清除。如果請求超時或出現錯誤,則保留requesttid,以便在請求最終完成後進行關聯(例如將請求從_error隊列移回服務隊列)。

最近的增強使此屬性成爲可選屬性,而不是使用實例的CorrelationId作爲請求消息RequestId。這可以簡化響應相關性,並且還避免了在saga repository上添加索引的需要。但是,在高度複雜的系統中,爲請求重用CorrelationId可能會導致問題。所以在選擇使用哪種方法時要考慮到這一點。

Configuration

要聲明請求,請添加request屬性並使用request方法對其進行配置。

public interface ProcessOrder
{
    Guid OrderId { get; }    
}

public interface OrderProcessed
{
    Guid OrderId { get; }
    Guid ProcessingId { get; }
}

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }

    public Guid? ProcessOrderRequestId { get; set; }
    public Guid? ProcessingId { get; set; }
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine(OrderStateMachineSettings settings)
    {
        Request(
            () => ProcessOrder,
            x => x.ProcessOrderRequestId, // Optional
            r => {
                r.ServiceAddress = settings.ProcessOrderServiceAddress;
                r.Timeout = settings.RequestTimeout;
            });
    }

    public Request<OrderState, ProcessOrder, OrderProcessed> ProcessOrder { get; private set; }
}

一旦定義, request活動就可以添加到行爲中。

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        During(Submitted,
            When(OrderAccepted)
                .Request(ProcessOrder, x => x.Init<ProcessOrder>(new { OrderId = x.Saga.CorrelationId}))
                .TransitionTo(ProcessOrder.Pending));

        During(ProcessOrder.Pending,
            When(ProcessOrder.Completed)
                .Then(context => context.Saga.ProcessingId = context.Message.ProcessingId)
                .TransitionTo(Processed),
            When(ProcessOrder.Faulted)
                .TransitionTo(ProcessFaulted),
            When(ProcessOrder.TimeoutExpired)
                .TransitionTo(ProcessTimeoutExpired));
    }

    public State Processed { get; private set; }
    public State ProcessFaulted { get; private set; }
    public State ProcessTimeoutExpired { get; private set; }
}

Request包括三個事件:Completed、Faulted和TimeoutExpired。這些事件可以在任何狀態中使用,但是,請求包含一個Pending狀態,可以使用它來避免聲明單獨的Pending狀態。

Missing Instance

如果在收到響應、錯誤或超時之前完成了saga實例,則可能會配置一個缺失的實例處理程序,類似於常規事件。

Request(() => ProcessOrder, x => x.ProcessOrderRequestId, r =>
{
    r.Completed = m => m.OnMissingInstance(i => i.Discard());
    r.Faulted = m => m.OnMissingInstance(i => i.Discard());
    r.TimeoutExpired = m => m.OnMissingInstance(i => i.Discard());
});

Custom

在某些情況下,事件行爲可能具有需要在作用域級別管理的依賴關係,例如數據庫連接,或者複雜性最好封裝在單獨的類中,而不是作爲狀態機本身的一部分。開發人員可以創建自己的活動以供狀態機使用,也可以選擇創建自己的擴展方法以將其添加到行爲中。

要創建一個activity,需要創建一個類來實現IStateMachineActivity<TInstance, TData> 如圖所示。

public class PublishOrderSubmittedActivity :
    IStateMachineActivity<OrderState, SubmitOrder>
{
    readonly ISomeService _service;

    public PublishOrderSubmittedActivity(ISomeService service)
    {
        _service = service;
    }

    public void Probe(ProbeContext context)
    {
        context.CreateScope("publish-order-submitted");
    }

    public void Accept(StateMachineVisitor visitor)
    {
        visitor.Visit(this);
    }

    public async Task Execute(BehaviorContext<OrderState, SubmitOrder> context, IBehavior<OrderState, SubmitOrder> next)
    {
        await _service.OnOrderSubmitted(context.Saga.CorrelationId);
        
        // always call the next activity in the behavior
        await next.Execute(context).ConfigureAwait(false);
    }

    public Task Faulted<TException>(BehaviorExceptionContext<OrderState, SubmitOrder, TException> context, 
        IBehavior<OrderState, SubmitOrder> next)
        where TException : Exception
    {
        return next.Faulted(context);
    }
}

對於ISomeService,在使用IPublishEndpoint發佈事件的類中實現接口,如下所示。

public class SomeService :
    ISomeService
{
    IPublishEndpoint _publishEndpoint;
    
    public SomeService(IPublishEndpoint publishEndpoint)
    {
        _publishEndpoint = publishEndpoint;
    }
    
    public async Task OnOrderSubmitted(Guid orderId)
    {
        await _publishEndpoint.Publish<OrderSubmitted>(new { OrderId = orderId });
    }
}

創建後,在狀態機中配置活動,如圖所示。

public interface OrderSubmitted
{
    Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .Activity(x => x.OfType<PublishOrderSubmittedActivity>())
                .TransitionTo(Submitted));
    }
}

當使用SubmitOrder事件時,狀態機將從容器中解析活動,並調用Execute方法。活動將被限定範圍,因此任何依賴都將在消息ConsumeContext中解析。

在上面的例子中,事件類型是事先已知的。如果需要任何事件類型的活動,則可以在不指定事件類型的情況下創建該活動。

public class PublishOrderSubmittedActivity :
    IStateMachineActivity<OrderState>
{
    readonly ISomeService _service;

    public PublishOrderSubmittedActivity(ISomeService service)
    {
        _service = service;
    }

    public void Probe(ProbeContext context)
    {
        context.CreateScope("publish-order-submitted");
    }

    public void Accept(StateMachineVisitor visitor)
    {
        visitor.Visit(this);
    }

    public async Task Execute(BehaviorContext<OrderState> context, IBehavior<OrderState> next)
    {
        await _service.OnOrderSubmitted(context.Saga.CorrelationId);

        await next.Execute(context).ConfigureAwait(false);
    }

    public async Task Execute<T>(BehaviorContext<OrderState, T> context, IBehavior<OrderState, T> next)
    {
        await _service.OnOrderSubmitted(context.Saga.CorrelationId);

        await next.Execute(context).ConfigureAwait(false);
    }

    public Task Faulted<TException>(BehaviorExceptionContext<OrderState, TException> context, IBehavior<OrderState> next) 
        where TException : Exception
    {
        return next.Faulted(context);
    }

    public Task Faulted<T, TException>(BehaviorExceptionContext<OrderState, T, TException> context, IBehavior<OrderState, T> next)
        where TException : Exception
    {
        return next.Faulted(context);
    }
}

要註冊實例活動,請使用以下語法。

public interface OrderSubmitted
{
    Guid OrderId { get; }    
}

public class OrderStateMachine :
    MassTransitStateMachine<OrderState>
{
    public OrderStateMachine()
    {
        Initially(
            When(SubmitOrder)
                .Activity(x => x.OfInstanceType<PublishOrderSubmittedActivity>())
                .TransitionTo(Submitted));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章