Wydanie 5’tej wersji frameworka .NET zbliża się wielkimi krokami. W tym samym czasie pojawi się nowa wersja F#, która również będzie oznaczona numerem 5. Jedna z nowych właściwości języka to:

Allow implementing the same interface at different generic instantiations

Jest to kluczowa zmiana w kontekście implementacji Sagi frameworka NServiceBus.

Kod Sagi w 4’tej wersji F# trzeba rozbijać na osobne klasy, które implementują interfejs IAmStartedByMessages lub IHandleMessages z konkretnym typem generycznym. Szczegóły takiego sposobu implementacji opisałem w szóstej części praktycznego przewodnika pokazującego, w jaki sposób można połączyć framework NServiceBus oraz język F#.

Kod Sagi w 5’tej wersji F# można napisać w taki sam sposób jak kod napisany w C#, przedstawiony w dokumentacji frameworka. Dzięki temu implementacja upraszcza się i jest spójna w obydwu językach.

Porównajmy kod przed i po wprowadzeniu zmian.

NServiceBus Saga - F# 4

Kod dostępny jest na moim koncie GitHub

module ShippingPolicy

open NServiceBus
open Events
open NServiceBus.Logging
open System.Threading.Tasks
open Commands

type ShippingPolicyData() =
    inherit ContainSagaData()
    member val OrderId = "" with get,set
    member val IsOrderPlaced = false with get,set
    member val IsOrderBilled = false with get,set

let canShipOrder (policyData: ShippingPolicyData) =
    policyData.IsOrderPlaced && policyData.IsOrderBilled

let shipOrder orderId (context: IMessageHandlerContext) =
    let shipOrder = new ShipOrder(orderId)
    context.SendLocal(shipOrder)

type ShippingPolicyOrderPlaced() =
    inherit Saga<ShippingPolicyData>()
        override this.ConfigureHowToFindSaga(mapper: SagaPropertyMapper<ShippingPolicyData>) =
            mapper.ConfigureMapping<OrderPlaced>(fun message -> message.OrderId :> obj).ToSaga(fun sagaData -> sagaData.OrderId :> obj)
    
    static member log = LogManager.GetLogger<ShippingPolicyOrderPlaced>()
    
    interface IAmStartedByMessages<OrderPlaced> with
        member this.Handle(message, context) = 
            ShippingPolicyOrderPlaced.log.Info("OrderPlaced message received.")
            
            this.Data.IsOrderPlaced <- true
            
            let canShipOrder = canShipOrder this.Data
            match canShipOrder with
            | true ->
                this.MarkAsComplete()
                shipOrder this.Data.OrderId context
            | false ->
                Task.CompletedTask

type ShippingPolicyOrderBilled() =
    inherit Saga<ShippingPolicyData>()
        override this.ConfigureHowToFindSaga(mapper: SagaPropertyMapper<ShippingPolicyData>) =
            mapper.ConfigureMapping<OrderBilled>(fun message -> message.OrderId :> obj).ToSaga(fun sagaData -> sagaData.OrderId :> obj)
            
    static member log = LogManager.GetLogger<ShippingPolicyOrderBilled>()
    
    interface IAmStartedByMessages<OrderBilled> with
        member this.Handle(message, context) = 
            ShippingPolicyOrderBilled.log.Info("OrderBilled message received.")
            
            this.Data.IsOrderBilled <- true
            
            let canShipOrder = canShipOrder this.Data
            match canShipOrder with
            | true ->
                this.MarkAsComplete()
                shipOrder this.Data.OrderId context
            | false ->
                Task.CompletedTask

Szczegółowy opis znajdziesz we wspomnianym już praktycznym przewodniku. Zwróć uwagę na konieczność utworzenia dwóch klas, które składają się na implementację Sagi NServiceBus:

  • ShippingPolicyOrderPlaced - przetwarza wiadomość OrderPlaced
  • ShippingPolicyOrderBilled - przetwarza wiadomość OrderBilled

Dodatkowo istnieją dwie osobne funkcje, które wykorzystywane są w obydwu klasach:

  • canShipOrder - zwraca informację, czy zamówienie może zostać wysłane
  • shipOrder - wysyła wiadomość, która inicjuje Policy wysyłki zamówienia

A jak może wyglądać implementacja tej samej funkcjonalności w nowej wersji języka F#? Zobaczmy.

NServiceBus Saga - F# 5

Kod dostępny jest na moim koncie GitHub

module ShippingPolicy

open NServiceBus
open Events
open NServiceBus.Logging
open System.Threading.Tasks
open Commands

type ShippingPolicyData() =
    inherit ContainSagaData()
    member val OrderId = "" with get,set
    member val IsOrderPlaced = false with get,set
    member val IsOrderBilled = false with get,set

type ShippingPolicy() =
    inherit Saga<ShippingPolicyData>()
        override this.ConfigureHowToFindSaga(mapper: SagaPropertyMapper<ShippingPolicyData>) =
            mapper.ConfigureMapping<OrderPlaced>(fun message -> message.OrderId :> obj).ToSaga(fun sagaData -> sagaData.OrderId :> obj)
            mapper.ConfigureMapping<OrderBilled>(fun message -> message.OrderId :> obj).ToSaga(fun sagaData -> sagaData.OrderId :> obj)

    static member log = LogManager.GetLogger<ShippingPolicy>()

    interface IAmStartedByMessages<OrderPlaced> with
        member this.Handle(message, context) =
            ShippingPolicy.log.Info("OrderPlaced message received.")

            this.Data.IsOrderPlaced <- true
            this.ProcessOrder(context)

    interface IAmStartedByMessages<OrderBilled> with
        member this.Handle(message, context) =
            ShippingPolicy.log.Info("OrderBilled message received.")

            this.Data.IsOrderBilled <- true
            this.ProcessOrder(context)

    member this.ProcessOrder(context:IMessageHandlerContext) =
        match this.Data.IsOrderPlaced && this.Data.IsOrderBilled with
        | true ->
            this.MarkAsComplete()
            let shipOrder = new ShipOrder(this.Data.OrderId)
            context.SendLocal(shipOrder)
        | false ->
            Task.CompletedTask

Do implementacji NServiceBus Saga, z wykorzystaniem nowych możliwości języka F#, wystarczy jedna klasa, dziedzicząca dwa razy po tym samym interfejsie IAmStartedByMessages, który różni się tylko parametrem generycznym:

  • IAmStartedByMessages<OrderPlaced> - przetwarzanie wiadomości typu OrderPlaced
  • IAmStartedByMessages<OrderBilled> - przetwarzanie wiadomości typu OrderBilled

Ponieważ całość może znajdować się w jednej klasie, możemy uprościć kod, przenosząc logikę działania funkcji canShipOrder oraz shipOrder do metody ProcessOrder, która znajduje się w samej klasie.

F# 5 wprowadza wiele innych przydatnych nowości, natomiast z punktu widzenia implementacji Sagi zmiana opisana w tym artykule jest najistotniejsza. Jeśli NServiceBus Saga jest ważną częścią zaprojektowanych rozwiązań, to możliwość dziedziczenia po tym samym interfejsie różniącym się parametrem generycznym zachęca do kodowania całych rozwiązań w języku F#. Daje to ciekawą perspektywę na przyszłość :)

Tymczasem udanego oglądania materiałów z nadchodzącej konferencji!

=