Po omówieniu komend pora na przejście do zapytań. Ich celem jest odczytywanie danych i z wracanie w odpowiedniej do wymagania formie.
Do wykonywania zapytań posłuży szyna zapytań. Dzięki jej zastosowaniu wywołanie zapytania odbywać się może w dowolnym miejscu aplikacji ze wstrzykniętą odpowiednią zależnością.
Wykonanie handlera zapytania odbędzie się zawsze w tym samym środowisku (szyna), zawsze będzie opakowane tym samym algorytmem i nie trzeba tutaj się martwić np. o stosowanie wyjątków, logi, gdyż w trakcie wykonywania przez szynę zapytania będzie to standardowo obsłużone.
Zapytania i szyna zapytań
Interfejs bazowy dla zapytań, musi zawierać typ generyczny jaki zostanie zwrócony.
IQuery baza dla wszystkich zapytań.
TResult - typ generyczny, określający zwracany wynik wykonania zapytania.
Interfejs zapytań.
1
2
3
4
5
6
namespace PictOgr.Core.CQRS.Query
{
public interface IQuery<TResult>
{
}
}
IQueryHandler - podobnie jak poprzednio (ICommandHandler), implementując ten kontrakt tworzymy kod przetwarzający logikę, a następnie zwracany jako typ generyczny TResult.
Interfejs handlera zapytania.
1
2
3
4
5
6
7
namespace PictOgr.Core.CQRS.Query
{
public interface IQueryHandler<in TQuery, out TResult> where TQuery : IQuery<TResult>
{
TResult Execute(TQuery query);
}
}
IQueryBus, kontrakt dla szyny danych, analogicznie jak dla komend z tą różnicą, iż trzeba uwzględnić typ generyczny TResult.
Interfejs szyny zapytań.
1
2
3
4
5
6
7
8
9
using PictOgr.Core.CQRS.Query;
namespace PictOgr.Core.CQRS.Bus
{
public interface IQueryBus
{
TResult Process<TQuery, TResult>(TQuery query) where TQuery : IQuery<TResult>;
}
}
QueryBus to implementacja interfejsu IQueryBus.
Implementacja szyny zapytań.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using Autofac;
using Autofac.Extras.NLog;
using PictOgr.Core.CQRS.Query;
namespace PictOgr.Core.CQRS.Bus
{
public class QueryBus : IQueryBus
{
private readonly ILifetimeScope container;
private readonly ILogger logger;
public QueryBus(ILifetimeScope container, ILogger logger)
{
this.container = container;
this.logger = logger;
}
public TResult Process<TQuery, TResult>(TQuery query) where TQuery : IQuery<TResult>
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var queryHandle = container.Resolve<IQueryHandler<TQuery, TResult>>();
if (queryHandle == null)
{
throw new Exception($"Not found handler for Query: '{query.GetType().FullName}'");
}
var result = default(TResult);
try
{
result = queryHandle.Execute(query);
}
catch (Exception e)
{
logger.Error(e);
}
return result;
}
}
}
Proces przetwarzania komendy można przedstawić w następujący sposób: Przekazanie zapytania do szyny przy pomocy wywołania metody Process. Jeżeli przekazane zapytanie jest pusta, to rzuca wyjątkiem ArgumentNullException, Wyciągnięcie z kontenera (Autofac) handlera dla przekazanego zapytania. Jeżeli handler jest pusty to rzuca wyjątek Exception. Tworzenie typu zwracanego z domyślnymi wartościami. Wywołanie zapytania poprzez metodę Execute w bloku try…catch. Jeżeli wywołanie się nie powiedzie to zostanie zapisany log błędu. Zwracanie wyniku przetwarzania zapytania.
Pierwsze zapytanie
Poniżej znajduje się pierwsza implementacja zapytania GetApplicationInformation, jako typ generyczny wykorzystana jest klasa ApplicationInformation, i to właśnie taki obiekt zostanie zwrócony po wykonaniu zapytania.
Implementacja zapytania pobierani informacji o aplikacji.
1
2
3
4
5
6
7
8
9
using PictOgr.Core.CQRS.Query;
using PictOgr.Core.Models;
namespace PictOgr.Core.Queries
{
public class GetApplicationInformation : IQuery<ApplicationInformation>
{
}
}
Za logikę wykonania zapytania odpowiedzialny jest handler GetApplicationInformationHandler. Jego celem jest pobranie z Assembly aktualnej wersji programu, a następnie zwrócenie jako klasę ApplicationInformation wyniku wykonania zapytania w metodzie Execute.
Implementacja handlera zapytania pobierania informacji o aplikacji (wersja).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Reflection;
using PictOgr.Core.CQRS.Query;
using PictOgr.Core.Models;
namespace PictOgr.Core.Queries
{
public class GetApplicationInformationHandler : IQueryHandler<GetApplicationInformation, ApplicationInformation>
{
public ApplicationInformation Execute(GetApplicationInformation query)
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
return new ApplicationInformation($"{version.Major}.{version.Minor}.{version.Build}");
}
}
}
Model jaki jest zwracany po wykonaniu zapytania GetApplicationInformation.
Model informacji aplikacji.
1
2
3
4
5
6
7
8
9
10
11
12
namespace PictOgr.Core.Models
{
public class ApplicationInformation
{
public ApplicationInformation(string version)
{
Version = version;
}
public string Version { get; private set; }
}
}
Użycie zapytania pobierania informacji aplikacji wygląda następująco:
1
var applicationInformation = QueryBus.Process<GetApplicationInformation, ApplicationInformation>(new GetApplicationInformation());
Testy
Automatyczne tworzenie obiektów przez Autofac przyczynia się do potrzeby nadpisania handlera dla testowanego zapytania.
Tak jak w przypadku komend, należy przygotować mocka dla interfejsu IQueryHandler.
Dodatkowo określamy co ma zwracać metoda Execute, a dokładniej, z jakiego delegata ma skorzystać po jej wywołaniu.
Dlatego każdy test korzystający z klasy QueryBaseTests, musi zaimplementować delegata. To pozwoli na dowolną imitację działania logiki zapytań.
Po przygotowaniu obiektu fake dla handlera, ponownie rejestrujemy w kontenerze.
Po tej operacji oryginalna metoda Execute z handlera jest już nadpisana, i możemy śmiało testować logikę wykonywanych zapytań.
Baza do testowania zapytań.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace PictOgr.Tests.Core.CQRS.Queries
{
using System;
using Autofac;
using FakeItEasy;
using PictOgr.Core.AutoFac;
using PictOgr.Core.CQRS.Bus;
using PictOgr.Core.CQRS.Query;
public class QueryBaseTests<TQuery, TResult> where TQuery : IQuery<TResult>
{
protected IQueryBus queryBus;
protected Func<TResult> handleMethod;
public QueryBaseTests()
{
var builder = Container.CreateBuilder();
var fakeHandler = A.Fake<IQueryHandler<TQuery, TResult>>();
A.CallTo(() => fakeHandler.Execute(A<TQuery>._)).ReturnsLazily(() => handleMethod.Invoke());
builder.Register(c => fakeHandler).AsImplementedInterfaces();
var container = builder.Build();
queryBus = container.Resolve<IQueryBus>();
}
}
}
Implementacja pierwszego testu, do generowania losowych ciągów znaków została wykorzystana biblioteka AutoFixture, spowoduje to losowość w trakcie uruchamiania testu.
Do prawidłowego działania testu należy ustawić delegat handleMethod (podobnie jak w przypadku testowania komend). W tym przypadku wykorzystujemy lambdę, i ustawiamy klasę jaka zostanie zwrócona przy wywołaniu metody Process z handlera dla szyny zapytań.
Testowanie zapytania pobierania informacji aplikacji.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace PictOgr.Tests.Core.CQRS.Queries
{
using PictOgr.Core.Models;
using PictOgr.Core.Queries;
using Ploeh.AutoFixture;
using Shouldly;
using Xunit;
public class GetApplicationInformationTest : QueryBaseTests<GetApplicationInformation, ApplicationInformation>
{
private readonly Fixture fixture;
public GetApplicationInformationTest()
{
fixture = new Fixture();
}
[Fact]
public void get_application_version_should_return_the_random_string()
{
var version = fixture.Create<string>();
handleMethod = () => new ApplicationInformation(version);
var applicationInformation = queryBus.Process<GetApplicationInformation, ApplicationInformation>(new GetApplicationInformation());
applicationInformation.Version.ShouldBe(version);
}
}
}
Po wykonaniu zapytania można sprawdzić wynik przy pomocy asercji: applicationInformation.Version.ShouldBe(version);.
Na koniec
Po implementacji komend i zapytań, mamy do dyspozycji bardzo potężny mechanizm, który można wykorzystać jako filar do budowy wielu aplikacji.
Wcześniej nigdy nie korzystałem z tego podejścia, nie myślałem nawet o takiej formie.
Moja interpretacja i implementacja zapewne odbiega od ideału.
Kolejnym krokiem będzie wykorzystanie Event Sourcing, i być może dodanie walidatorów, jednak to jest już temat na kolejny post.
Rozważam, możliwość wyodrębnienie a projektu PictOgr, implementacji CQRS, tak by w przyszłości można było wielokrotnie wykorzystywać efekty mojej pracy.
Dziękuję za wytrwałość i zachęcam do komentowania.
Jest to post przygotowany na potrzeby konkursu „Daj Się Poznać 2017” organizowanym przez Macieja Aniserowicza.
Blog | https://mrdev.pl |
Projekt | https://mrdev.pl/pictogr-pomysl |
GitHub | github.com/krzysztofowsiany/pictogr |
Snapchat | www.snapchat.com/add/gocom7 |
www.facebook.com/PictOgr-1729700930654225 | |
twitter.com/gemu_gocom | |
RSS | http://mrdev.pl/category/daj-sie-poznac-2017/feed |