Posty z tej serii:

W ostatniej części praktycznego przewodnika pokazującego, w jaki sposób można połączyć język F# z frameworkiem NServiceBus zdefiniowałem pojedynczą odpowiedzialność:

  • C# do Messagingu i Queueingu
  • F# do pozostałych elementów

Z taką koncepcją rozpoczynałem przebudowę systemu komentarzy, którego używam na tym blogu. W ten sposób po stronie C# zostały:

  • konfiguracja Endpointa NServiceBus
  • implementacja komponentów NServiceBus
  • hosting Endpointa NServiceBus
  • implementacja komponentu Webowego
  • hosting komponentu Webowego
  • część testów integracyjnych

Na stronę F# przeszły:

  • implementacja GitHub API
  • definicje kontraktów wiadomości NServiceBus
  • logika systemu
  • część testów integracyjnych
  • wszystkie testy jednostkowe

Przejdźmy przez realizację wybranych elementów.

F# - NServiceBus - kontrakty

Policy Comment Registration inicjalizowane jest przez wiadomość typu Command o nazwie RegisterComment. W NServiceBus wiadomość definiuje się poprzez klasę. Język F# umożliwia definiowanie klas, które są rozpoznawane przez język C#. Dzięki temu kontrakty mogą być definiowane w pierwszym języku, a używane w drugim języku.

Definicja RegisterComment wygląda tak:

namespace Bc.Contracts.Internals.Endpoint.CommentRegistration.Commands

    open System

    type RegisterComment(
                        commentId: Guid,
                        userName: string,
                        userWebsite: string,
                        userComment: string,
                        articleFileName: string,
                        commentAddedDate: DateTime
                    ) =
        member this.CommentId = commentId
        member this.UserName = userName
        member this.UserWebsite = userWebsite
        member this.UserComment = userComment
        member this.ArticleFileName = articleFileName
        member this.CommentAddedDate = commentAddedDate


Comment Registration kończąc realizację swojej odpowiedzialności, publikuje wiadomość typu Event o nazwie CommentRegistered:

namespace Bc.Contracts.Externals.Endpoint.CommentRegistration.Events

    open System

    type CommentRegistered(commentId: Guid, commentUri: string) =
        member this.CommentId = commentId
        member this.CommentUri = commentUri


A tak wyglądają kontrakty dla Comment Answer Notification:

namespace Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Commands

    open System

    type RegisterCommentNotification(commentId: Guid, userEmail: string, articleFileName: string) =
        member this.CommentId = commentId
        member this.UserEmail = userEmail
        member this.ArticleFileName = articleFileName
        
    type NotifyAboutCommentAnswer(commentId: Guid, isApproved: bool) =
        member this.CommentId = commentId
        member this.IsApproved = isApproved

Dlaczego dwie wiadomości typu Command skoro diagram pokazuje jedno połączenie? Wrócimy do tego w późniejszej części artykułu.


Zobaczmy, w jaki sposób można definiować wiadomość typu Message. Za przykład weźmy Policy GitHub Pull Request Creation:

namespace Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Messages

    open System

    type RequestCreateGitHubPullRequest(
                                           commentId: Guid,
                                           fileName: string,
                                           content: string,
                                           addedDate: DateTime
                                       ) =
        member this.CommentId = commentId
        member this.FileName = fileName
        member this.Content = content
        member this.AddedDate = addedDate


Definicje kontraktów dla pozostałych Policies wyglądają analogicznie. Przyglądnij się końcówką nazw namespace dla każdego z typów wiadomości. Jest to zdefiniowana konwencja, która instruuje NServiceBusa o typie wiadomości do przetworzenia.

F# - Logika - interfejsy

Interfejs jest łącznikiem pomiędzy kodem napisanym w C# a kodem napisanym w F#. Interfejsy mogą być zdefiniowane w którymkolwiek języku. Postanowiłem umieścić je po stronie F#.

Interfejs logiki dla Comment Registration wygląda tak:

namespace Bc.Contracts.Internals.Endpoint.CommentRegistration.Logic

    open System

    type ICommentRegistrationPolicyLogic =
        abstract member FormatUserName: userName: string -> userWebsite: string -> string
        abstract member FormatUserComment: userName: string -> userComment: string -> commentAddedDate: DateTime -> string

Metoda FormatUserName formatuje nazwę użytkownika. Metoda FormatUserComment formatuje przesłany komentarz. W takiej postaci dane pojawiają się na GitHubie.


Zobaczmy definicję interfejsu logiki dla Comment Answer Notification:

namespace Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Logic

    type ICommentAnswerNotificationPolicyLogic =
        abstract member From: string
        abstract member Subject: string
        abstract member GetBody: articleFileName: string -> string
        abstract member IsSendNotification: isCommentApproved: bool -> userEmail: string -> bool

Główną odpowiedzialnością logiki jest zwrócenie informacji o tym, czy powiadomienie powinno być wysłane - metoda IsSendNotification. Pozostałe elementy zwracają dane potrzebne do wysłania komunikatu.


Na koniec przyjrzyjmy się definicji interfejsu logiki dla GitHub Pull Request Creation:

namespace Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Logic

    open System
    open System.Threading.Tasks

    type IGitHubPullRequestCreationPolicyLogic =
        abstract member CreateBranch: creationDate: DateTime -> Task<string>
        abstract member UpdateFile: branchName: string -> fileName: string -> content: string -> Task
        abstract member CreatePullRequest: branchName: string -> Task<string>

Celem logiki jest zgłoszenie Pull Requesta na GitHubie. Całość podzielona jest na trzy osobne kroki:

  • utworzenie brancha
  • aktualizacja pliku
  • utworzenie Pull Requesta.


Definicje interfejsów dla pozostałych Policies wyglądają analogicznie. Ponownie, przyjrzyj się końcówce w nazwach namespace. W ten sposób pogrupowane są kontrakty związane z interfejsami logiki.

F# - Logika - implementacja

Implementacja interfejsów wygląda tak samo, jak w języku C#. Trzeba utworzyć klasę, która dziedziczy po interfejsie, a następnie zaimplementować funkcjonalność zgodnie z jego sygnaturą. Zobaczmy szczegóły na trzech, znanych nam już przykładach. Na początek logika dla Comment Registration:

namespace Bc.Logic.Endpoint.CommentRegistration

open System
open Bc.Contracts.Internals.Endpoint.CommentRegistration.Logic

type CommentRegistrationPolicyLogic() =
    interface ICommentRegistrationPolicyLogic with
        member this.FormatUserName userName userWebsite =
            match userWebsite with
            | null -> sprintf "**%s**" userName
            | "" -> sprintf "**%s**" userName
            | _ -> sprintf "[%s](%s)" userName userWebsite


        member this.FormatUserComment userName userComment commentAddedDate =
            sprintf "begin-%s-%s-%s UTC %s" userName userComment (commentAddedDate.ToString("yyyy-MM-dd HH:mm")) Environment.NewLine

ICommentRegistrationPolicyLogic jest pomostem pomiędzy językiem C#, a językiem F#, dlatego implementacja CommentRegistrationPolicyLogic musi uwzględniać specyfikę pierwszego z nich, jak np. obsługa wartości null.


Logika dla Comment Answer Notification wygląda tak:

namespace Bc.Logic.Endpoint.CommentAnswerNotification

open System
open System.Linq
open System.Configuration
open Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Logic

module private ConfigurationProvider =
    let SmtpFrom = ConfigurationManager.AppSettings.["SmtpFrom"]

    let BlogDomainName = ConfigurationManager.AppSettings.["BlogDomainName"]

module GetBody =
    let execute (fileName: String) (blogDomainName: string) =

        // ...

        result

type CommentAnswerNotificationPolicyLogic() =
    interface ICommentAnswerNotificationPolicyLogic with
        member this.From = ConfigurationProvider.SmtpFrom

        ////TODO: move to resource file
        member this.Subject = "Dodano odpowiedź do komentarza."
        member this.GetBody fileName = GetBody.execute fileName ConfigurationProvider.BlogDomainName
        member this.IsSendNotification isCommentApproved userEmail =
            not(System.String.IsNullOrEmpty(userEmail)) && isCommentApproved

Implementując logikę, możemy wykorzystywać dobrodziejstwa języka F# jak np. dzielenie kodu na moduły. W powyższym przykładzie pominąłem szczegóły implementacji metody GetBody.execute.


Na koniec zobaczmy kod dla GitHub Pull Request Creation:

namespace Bc.Logic.Endpoint.GitHubPullRequestCreation

open System.Threading.Tasks
open Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Logic
open GitHubApi

type GitHubPullRequestCreationPolicyLogic() =
        interface IGitHubPullRequestCreationPolicyLogic with
            member this.CreateBranch creationDate =
                async {
                    let branchName = sprintf "c-%s" (creationDate.ToString("yyyy-MM-dd-HH-mm-ss-fff"))
                    do! GitHubApi.CreateRepositoryBranch.execute
                            GitHubConfigurationProvider.userAgent
                            GitHubConfigurationProvider.authorizationToken
                            GitHubConfigurationProvider.repositoryName
                            GitHubConfigurationProvider.masterBranchName
                            branchName
                    return branchName
                } |> Async.StartAsTask

            member this.UpdateFile branchName fileName content =
                async {
                   // ...
                } |> Async.StartAsTask :> Task

            member this.CreatePullRequest branchName  =
                async {
                    // ...
                    return pullRequestUri
                } |> Async.StartAsTask

Ponownie, dla lepszej czytelności, pominąłem kod dla metod this.UpdateFile oraz this.CreatePullRequest. Ich implementacja wygląda analogicznie jak dla metody this.CreateBranch, która konstruuje nazwę, a następnie tworzy brancha za pomocą metody GitHubApi.CreateRepositoryBranch.execute.

F# - Testy

Zanim przejdziemy do szczegółów implementacji w języku C#, zobaczmy, jak może wyglądać kod testu jednostkowego:

module Bc.Logic.Endpoint.Tests.CommentRegistration

open System
open Bc.Contracts.Internals.Endpoint.CommentRegistration.Logic
open Bc.Logic.Endpoint.CommentRegistration
open NUnit.Framework

module CommentRegistrationPolicyLogicTests =

    let getLogic () =
        CommentRegistrationPolicyLogic()

    [<TestCase("user", null, "**user**")>]
    [<TestCase("user", "", "**user**")>]
    [<TestCase("user", "webSite", "[user](webSite)")>]
    let FormatUserName_Execute_ProperResult(userName, userWebsite, expectedResult) =

        // Arrange
        let logic = getLogic () :> ICommentRegistrationPolicyLogic

        // Act
        let result = logic.FormatUserName userName userWebsite

        // Assert
        Assert.That(result, Is.EqualTo(expectedResult))

Zasada działania jest taka sama jak dla testów napisanych w C#.

C# - NServiceBus - Konfiguracja Endpointa

Kod F# dołącza się do kodu C# w taki sam sposób, jak łączenie kodu C# <-> C#. Do projektu .csproj dołącza się projekt .fsproj. Interfejsy oraz klasy napisane w F# są rozpoznawane przez C#. Zobaczmy, jak to działa na przykładzie konfigurowania zależności w kontenerze udostępnianym przez NServiceBus:

// dependency injection
endpoint.RegisterComponents(reg =>
{
    if (configurationProvider.IsUseFakes)
    {
        reg.ConfigureComponent<GitHubPullRequestVerificationPolicyLogicFake>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<GitHubPullRequestCreationPolicyLogicFake>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentAnswerPolicyLogicFake>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentAnswerNotificationPolicyLogicFake>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentRegistrationPolicyLogicFake>(DependencyLifecycle.InstancePerCall);
    }
    else
    {
        reg.ConfigureComponent<GitHubPullRequestVerificationPolicyLogic>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<GitHubPullRequestCreationPolicyLogic>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentAnswerPolicyLogic>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentAnswerNotificationPolicyLogic>(DependencyLifecycle.InstancePerCall);
        reg.ConfigureComponent<CommentRegistrationPolicyLogic>(DependencyLifecycle.InstancePerCall);
    }
});

Zmienna endpoint służy do konfiguracji różnych właściwości NServiceBusa. Metoda RegisterComponents pozwala wstrzykiwać zależności, które można wykorzystywać w Message Handlerach. Wstrzykiwane klasy zakodowane są w języku F#.

C# - NServiceBus - Policy - Saga

W poprzedniej części wspomniałem, że zaprojektowane Policy zgodnie z podejściem ADSD najlepiej implementuje się jako Sagę NServiceBusa. Zobaczmy więc, jak taka implementacja może wyglądać. Na początek Comment Registration Policy:

using System;
using System.Threading.Tasks;
using Bc.Contracts.Externals.Endpoint.CommentRegistration.Events;
using Bc.Contracts.Internals.Endpoint.CommentRegistration.Commands;
using Bc.Contracts.Internals.Endpoint.CommentRegistration.Logic;
using Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Messages;
using NServiceBus;

namespace Bc.Endpoint
{
    public class CommentRegistrationPolicy :
        Saga<CommentRegistrationPolicy.PolicyData>,
        IAmStartedByMessages<RegisterComment>,
        IHandleMessages<ResponseCreateGitHubPullRequest>
    {
        private readonly ICommentRegistrationPolicyLogic logic;

        public CommentRegistrationPolicy(ICommentRegistrationPolicyLogic logic)
        {
            this.logic = logic;
        }

        public Task Handle(RegisterComment message, IMessageHandlerContext context)
        {
            this.Data.UserName = message.UserName;
            this.Data.UserWebsite = message.UserWebsite;
            this.Data.UserComment = message.UserComment;
            this.Data.ArticleFileName = message.ArticleFileName;
            this.Data.CommentAddedDate = message.CommentAddedDate;

            var formatUserName = this.logic.FormatUserName(this.Data.UserName, this.Data.UserWebsite);
            var formatUserComment =
                this.logic.FormatUserComment(formatUserName, this.Data.UserComment, this.Data.CommentAddedDate);

            return context.Send(new RequestCreateGitHubPullRequest(
                message.CommentId,
                this.Data.ArticleFileName,
                formatUserComment,
                this.Data.CommentAddedDate));
        }

        public Task Handle(ResponseCreateGitHubPullRequest message, IMessageHandlerContext context)
        {
            this.MarkAsComplete();
            return context.Publish(new CommentRegistered(this.Data.CommentId, message.PullRequestUri));
        }

        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<PolicyData> mapper)
        {
            mapper.ConfigureMapping<RegisterComment>(message => message.CommentId)
                  .ToSaga(data => data.CommentId);

            mapper.ConfigureMapping<ResponseCreateGitHubPullRequest>(message => message.CommentId)
                .ToSaga(data => data.CommentId);
        }

        public class PolicyData : ContainSagaData
        {
            public Guid CommentId { get; set; }

            public string UserName { get; set; }

            public string UserWebsite { get; set; }

            public string UserComment { get; set; }

            public string ArticleFileName { get; set; }

            public DateTime CommentAddedDate { get; set; }
        }
    }
}

Flow CommentRegistrationPolicy wygląda następująco:

  • wiadomość RegisterComment tworzy nową instancję Sagi
  • handler wiadomości wykonuje logikę biznesową, a następnie wysyła żądanie utworzenia GitHub Pull Requesta
  • Saga, po otrzymaniu odpowiedzi, publikuje zdarzenie CommentRegistered, rozgłaszając zarejestrowanie komentarza

Interfejs logiki oraz kontrakty używane są w taki sam sposób, jakby były napisane w C#, mimo, że zostały zakodowane w F#.


A tak wygląda przetwarzanie wiadomości dla Policy Comment Answer Notification:

using System;
using System.Threading.Tasks;
using Bc.Contracts.Externals.Endpoint.CommentAnswer.Events;
using Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Commands;
using Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Logic;
using NServiceBus;
using NServiceBus.Mailer;

namespace Bc.Endpoint
{
    public class CommentAnswerNotificationEventSubscribingPolicy :
        IHandleMessages<CommentApproved>,
        IHandleMessages<CommentRejected>
    {
        public Task Handle(CommentApproved message, IMessageHandlerContext context)
        {
            return context.Send(new NotifyAboutCommentAnswer(message.CommentId, true));
        }

        public Task Handle(CommentRejected message, IMessageHandlerContext context)
        {
            return context.Send(new NotifyAboutCommentAnswer(message.CommentId, false));
        }
    }

    public class CommentAnswerNotificationPolicy :
        Saga<CommentAnswerNotificationPolicy.PolicyData>,
        IAmStartedByMessages<RegisterCommentNotification>,
        IAmStartedByMessages<NotifyAboutCommentAnswer>
    {
        private readonly ICommentAnswerNotificationPolicyLogic logic;

        public CommentAnswerNotificationPolicy(ICommentAnswerNotificationPolicyLogic logic)
        {
            this.logic = logic;
        }

        public Task Handle(RegisterCommentNotification message, IMessageHandlerContext context)
        {
            this.Data.UserEmail = message.UserEmail;
            this.Data.ArticleFileName = message.ArticleFileName;
            this.Data.IsNotificationRegistered = true;

            return this.SendNotification(context);
        }

        public Task Handle(NotifyAboutCommentAnswer message, IMessageHandlerContext context)
        {
            this.Data.IsCommentApproved = message.IsApproved;
            this.Data.IsNotificationReadyToSend = true;

            return this.SendNotification(context);
        }

        private Task SendNotification(IMessageHandlerContext context)
        {
            if (!this.Data.IsNotificationRegistered || !this.Data.IsNotificationReadyToSend)
            {
                return Task.CompletedTask;
            }

            this.MarkAsComplete();

            if (!this.logic.IsSendNotification(this.Data.IsCommentApproved, this.Data.UserEmail))
            {
                return Task.CompletedTask;
            }

            var mail = new Mail
            {
                From = this.logic.From,
                To = this.Data.UserEmail,
                Subject = this.logic.Subject,
                Body = this.logic.GetBody(this.Data.ArticleFileName)
            };

            return context.SendMail(mail);
        }

        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<PolicyData> mapper)
        {
            mapper.ConfigureMapping<RegisterCommentNotification>(message => message.CommentId)
                  .ToSaga(data => data.CommentId);

            mapper.ConfigureMapping<NotifyAboutCommentAnswer>(message => message.CommentId)
                  .ToSaga(data => data.CommentId);
        }

        public class PolicyData : ContainSagaData
        {
            public Guid CommentId { get; set; }

            public string UserEmail { get; set; }

            public string ArticleFileName { get; set; }

            public bool IsCommentApproved { get; set; }

            public bool IsNotificationRegistered { get; set; }

            public bool IsNotificationReadyToSend { get; set; }
        }
    }
}

Wróćmy do tematu dwóch wiadomości typu Command dla powyższego Policy. Saga może być wystartowana przez jedną z wiadomości:

  • RegisterCommentNotification
  • NotifyAboutCommentAnswer

Pierwszą wiadomość wysyła Comment Taking w momencie rejestrowania komentarza. Drugą wiadomość wysyła Policy Comment Answer Notification Event Subscribing. Jeśli popatrzysz na diagram z poprzedniej części, to nie znajdziesz tam ostatniego elementu. Dlaczego? Bardzo ważną zasadą ADSD jest rozdzielenie logicznej odpowiedzialności od fizycznej implementacji, a także fizycznego Deploymentu.

Logicznie, Comment Answer Notification nasłuchuje na Event od Comment Answer.

Fizycznie, NServiceBus desarilizuje pobraną wiadomość z kolejki, a następnie uruchamiana wszystkie Handlery, które obsługują rozpoznany typ wiadomości np. CommentApproved. Jeśli w jednym z nich wystąpi błąd, mechanizm re-try ponowi całe przetwarzanie, co spowoduje, że każdy z Handlerów wykona się ponownie. Jeśli nasze Handlery zostały zaprojektowane jako osobne elementy i mają się wykonywać niezależnie, to w tym, przypadku mamy zależność, której nie chcemy.

Rozwiązaniem jest minimalizowanie powstałej zależności, stąd klasa CommentAnswerNotificationEventSubscribingPolicy pełni rolę pośrednika, którego jedynym zadaniem jest nasłuchiwać na odpowiednie Eventy a następnie przekierowywać dane do CommentAnswerNotificationPolicy.

Jeśli w projekcie pojawi się inny komponent, który również będzie nasłuchiwał na Event CommentApproved, jego implementacja będzie wyglądać tak samo.

Kolejną różnicą pomiędzy logicznym projektem, a fizyczną realizacją jest to, że Comment Answer wysyła dwa osobne zdarzenia na poinformowanie o tym, czy komentarz został zaakceptowany, czy odrzucony:

  • CommentApproved
  • CommentRejected

Jest to szczegół implementacyjny, którego również nie chcemy mieć na wspomnianym diagramie. Innym możliwym rozwiązaniem byłoby wysyłanie jednego typu wiadomości ze statusem odpowiedzi.

Na koniec zobaczmy implementację GitHub Pull Request Creation:

using System;
using System.Threading.Tasks;
using Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Logic;
using Bc.Contracts.Internals.Endpoint.GitHubPullRequestCreation.Messages;
using NServiceBus;

namespace Bc.Endpoint
{
    public class GitHubPullRequestCreationPolicy :
        Saga<GitHubPullRequestCreationPolicy.PolicyData>,
        IAmStartedByMessages<RequestCreateGitHubPullRequest>,
        IHandleMessages<ResponseCreateBranch>,
        IHandleMessages<ResponseUpdateFile>,
        IHandleMessages<ResponseCreatePullRequest>
    {
        public Task Handle(RequestCreateGitHubPullRequest message, IMessageHandlerContext context)
        {
            this.Data.FileName = message.FileName;
            this.Data.Content = message.Content;
            this.Data.AddedDate = message.AddedDate;

            return context.Send(new RequestCreateBranch(this.Data.AddedDate));
        }

        public Task Handle(ResponseCreateBranch message, IMessageHandlerContext context)
        {
            this.Data.BranchName = message.BranchName;
            return context.Send(new RequestUpdateFile(this.Data.BranchName, this.Data.FileName, this.Data.Content));
        }

        public Task Handle(ResponseUpdateFile message, IMessageHandlerContext context)
        {
            return context.Send(new RequestCreatePullRequest(this.Data.BranchName));
        }

        public Task Handle(ResponseCreatePullRequest message, IMessageHandlerContext context)
        {
            this.MarkAsComplete();
            return this.ReplyToOriginator(
                context,
                new ResponseCreateGitHubPullRequest(this.Data.CommentId, message.PullRequestUri));
        }

        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<PolicyData> mapper)
        {
            mapper.ConfigureMapping<RequestCreateGitHubPullRequest>(message => message.CommentId)
                  .ToSaga(data => data.CommentId);
        }

        public class PolicyData : ContainSagaData
        {
            public Guid CommentId { get; set; }

            public string FileName { get; set; }

            public string Content { get; set; }

            public DateTime AddedDate { get; set; }

            public string BranchName { get; set; }
        }
    }

    public class GitHubPullRequestCreationPolicyHandlers :
        IHandleMessages<RequestCreateBranch>,
        IHandleMessages<RequestUpdateFile>,
        IHandleMessages<RequestCreatePullRequest>
    {
        private readonly IGitHubPullRequestCreationPolicyLogic logic;

        public GitHubPullRequestCreationPolicyHandlers(IGitHubPullRequestCreationPolicyLogic logic)
        {
            this.logic = logic;
        }

        public async Task Handle(RequestCreateBranch message, IMessageHandlerContext context)
        {
            var branchName = await this.logic.CreateBranch(message.AddedDate).ConfigureAwait(false);
            await context.Reply(new ResponseCreateBranch(branchName)).ConfigureAwait(false);
        }

        public async Task Handle(RequestUpdateFile message, IMessageHandlerContext context)
        {
            await this.logic.UpdateFile(message.BranchName, message.FileName, message.Content).ConfigureAwait(false);
            await context.Reply(new ResponseUpdateFile()).ConfigureAwait(false);
        }

        public async Task Handle(RequestCreatePullRequest message, IMessageHandlerContext context)
        {
            var pullRequestUri = await this.logic.CreatePullRequest(message.BranchName).ConfigureAwait(false);
            await context.Reply(new ResponseCreatePullRequest(pullRequestUri)).ConfigureAwait(false);
        }
    }
}

Myślę, że teraz już widzisz podział odpowiedzialności pomiędzy przetwarzaniem wiadomości a realizacją logiki biznesowej.

C# - NServiceBus - IT/OPS

Zobaczmy na implementację Comment Taking. Jest to punkt rozdzielający dane, przetwarzane przez różne usługi:

using System.Threading.Tasks;
using Bc.Contracts.Internals.Endpoint.CommentAnswerNotification.Commands;
using Bc.Contracts.Internals.Endpoint.CommentRegistration.Commands;
using Bc.Contracts.Internals.Endpoint.CommentTaking.Commands;
using NServiceBus;

namespace Bc.Endpoint
{
    public class CommentTakingPolicy : IHandleMessages<TakeComment>
    {
        public async Task Handle(TakeComment message, IMessageHandlerContext context)
        {
            await context.Send(new RegisterComment(
                message.CommentId,
                message.UserName,
                message.UserWebsite,
                message.UserComment,
                message.ArticleFileName,
                message.CommentAddedDate)).ConfigureAwait(false);
            
            await context.Send(new RegisterCommentNotification(
                message.CommentId,
                message.UserEmail,
                message.ArticleFileName)).ConfigureAwait(false);
        }
    }
}

Skomplikowane? Raczej nie :) Standardowe wysłanie wiadomości typu Command w odpowiednie miejsca z odpowiednimi danymi. Elementem łączącym wszystkie dane jest CommentId.

Pożegnanie z NancyFx

Od samego początku komponentem webowym, przyjmującym komentarz od użytkownika był komponent o nazwie Comment Module, zrealizowany za pomocą frameworka NancyFX. Framework ten przestał być utrzymywany dlatego naturalną decyzją było przejście na coś innego. Wybór nie powinien Cię zaskoczyć. Zdecydowałem się na ASP.NET, tym bardziej że NServiceBus wspiera konfigurowanie Endpointa za pomocą tzw. Generic Host. Przemianowałem też nazwę komponentu na Comment Controller.

Test & Deploy

W tych dwóch kwestiach w zasadzie nic się nie zmieniło. Techniki testowania pozostały te same:

  • automatyczne testy jednostkowe
  • manualne testy integracyjne.

Część kodu dla testów powędrowała na stronę języka F#.

Jeśli chodzi o wdrażanie, to bez zarzutu sprawdza się narzędzie Fake.

Podsumowanie

Doszliśmy do końca mini serii, w której miałeś/miałaś okazję zobaczyć, w jaki sposób można zaprojektować rozwiązanie wg. idei zawartych w krusie Advanced Distributed System Design (ADSD), a także sposób, w jaki można taki projekt zrealizować używając frameworka NServiceBus oraz języków C# i F#.

Całość implementacji znajdziesz na moim GitHubie. Zachowałem poprzednie rozwiązania w postaci branchy, aby móc analizować zmiany, jakie zachodziły w każdym kolejnym kroku rozwoju systemu.

=