PictOgr - mój CQRS -3-

12.04.2017

CQRS Niniejszy wpis dotyczy implementacji Event Sourcingu w moim CQRSie. Jest to kolejna szyna wykorzystywana na różne sposoby.

Można np. zachować (jeżeli system cały system oparty jest o CQRS/ES) stan aplikacji w poszczególnych etapach jej życia. Zapis stanów musi odbyć sie np. w bazie danych. Nie mniej jednak pozwoli to na przedstawienie historii od A do Z cyklu życia.

Dokładnie po wykonaniu każdej z komend jak zmienia się model aplikacji. Zdarzenia wyzalane są z komend.

Zastosowanie zdarzeń pozwoli na powiadamianie różnych obszarów aplikacji o zmianie stanu aplikacji.

Zdarzenia i szyna zdarzeń

IEvent interfejs bazowy dla wszystkich zapytań, klas implementująca zostanie przekazana do zarejestrowanych obserwatorów.

Interfejs zdarzeń.

1
2
3
4
5
6
namespace PictOgr.Core.CQRS.Event
{
	public interface IEvent
	{
	}
}

IEventHandler - podobnie jak poprzednio (IQueryHandler), implementując ten kontrakt tworzymy kod przetwarzający logikę.

Interfejs handlera zdarzeń.

1
2
3
4
5
6
7
namespace PictOgr.Core.CQRS.Event
{
	public interface IEventHandler<TEvent> where TEvent : IEvent
	{
		void Handle(TEvent @event);
	}
}

@event - jak wiadomo event to słowo kluczowe języka c#, jednak można to obejść wykorzystując znak małpki ‚@’, nakazujemy kompilatorowi, iż nie chcemy skorzystać ze słowa kluczowego event, a jedynie użyć jako nazway zmiennej.

IEventBus, kontrakt dla szyny zdarzeń, w tym przypadku klasa implementująca musi zawierać trzy metody:

  • Register - służy do rejestrowania obserwatora zdarzenia,
  • UnRegister - analogicznie tą metodą wyrzucamy obserwatora zdarzeń,
  • Publish - dzięki tej metodzie przekazujemy wszystkim słuchaczom zdarzenie.

Interfejs szyny zdarzeń.

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace PictOgr.Core.CQRS.Bus.Event
{
	using CQRS.Event;

	public interface IEventBus
	{
		void Register<TEvent>(IEventHandler<TEvent> eventHandler) where TEvent : IEvent;

		void UnRegister<TEvent>(IEventHandler<TEvent> eventHandler) where TEvent : IEvent;

		void Publish<TEvent>(TEvent @event) where TEvent : IEvent;
	}
}

EventBus to implementacja interfejsu IEventBus.

Implementacja szyny zdarzeń.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
namespace PictOgr.Core.CQRS.Bus.Event
{
	using System;
	using System.Collections.Generic;
	using Autofac.Extras.NLog;
	using CQRS.Event;

	public class EventBus : IEventBus
	{
		private readonly ILogger logger;

		private static Dictionary<Type, List<object>> eventList = new Dictionary<Type, List<object>>();

		public EventBus(ILogger logger)
		{
			this.logger = logger;
		}

		public void Register<TEvent>(IEventHandler<TEvent> eventHandler) where TEvent : IEvent
		{
			List<object> eventHandlers;

			if (!eventList.ContainsKey(typeof(TEvent)))
			{
				eventList.Add(typeof(TEvent), new List<object>());
			}

			if (!eventList.TryGetValue(typeof(TEvent), out eventHandlers))
			{
				return;
			}

			try
			{
				if (!eventHandlers.Contains(eventHandler))
				{
					eventHandlers.Add(eventHandler);
				}
			}
			catch (Exception e)
			{
				logger.Error(e);
			}
		}

		public void UnRegister<TEvent>(IEventHandler<TEvent> eventHandler) where TEvent : IEvent
		{
			List<object> eventHandlers;

			if (!eventList.TryGetValue(typeof(TEvent), out eventHandlers))
			{
				throw new TypeUnloadedException(nameof(TEvent));
			}

			try
			{
				eventHandlers.Remove(eventHandler);

				if (eventHandlers.Count == 0)
				{
					eventList.Remove(typeof(TEvent));
				}
			}
			catch (Exception e)
			{
				logger.Error(e);
			}
		}

		public void Publish<TEvent>(TEvent @event) where TEvent : IEvent
		{
			List<object> eventHandlers;
			if (!eventList.TryGetValue(typeof(TEvent), out eventHandlers))
			{
				throw new TypeUnloadedException(nameof(TEvent));
			}

			try
			{
				foreach (var eventHandler in eventHandlers)
				{
					(eventHandler as IEventHandler<TEvent>)?.Handle(@event);
				}
			}
			catch (Exception e)
			{
				logger.Error(e);
			}
		}
	}
}

Lista eventList = new Dictionary<Type, List>();

Jest to lista zdarzeń, do której zostaną, która to dopiero będzie powiązana z listą obserwatorów. CQRS

[IEvent => { IEventHandler, IEventHandler, IEventHandler, etc.}] Metoda Register, sprawdza czy IEventHanlder jest już zarejestrowany, jeśli nie to dodaje do listy. UnRegister, sprawdza czy IEventHandler jest na liście, jeżeli jest to usuwa. Publish - iteruje po wszystkich IEvent, oraz podległych IEventHandlerach i publikuje zdarzenie.

Testy

Klasę testu zaczynamy od ustawienia (TearUp) podstawowych/najczęściej używanych w testach obiektów jak container, fakeEvent, eventFakeInvoke, fakeEventHandler.

Klasa do testowania zdarzeń.

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
namespace PictOgr.Tests.Core.CQRS.Events
{
	using System.Collections.Generic;
	using System;
	using Autofac;
	using FakeItEasy;
	using PictOgr.Core.AutoFac;
	using PictOgr.Core.CQRS.Bus.Event;
	using PictOgr.Core.CQRS.Event;
	using Shouldly;
	using Xunit;

	public class EventdBusTest : IDisposable
	{
		private readonly IContainer container;
		private readonly IEvent eventFromInvoke;
		private readonly IEvent fakeEvent;
		private readonly IEventHandler&lt;IEvent&gt; fakeEventHandler;

		public EventdBusTest()
		{
			container = Container.CreateBuilder().Build();

			fakeEvent = A.Fake&lt;IEvent&gt;();

			fakeEventHandler = A.Fake&lt;IEventHandler&lt;IEvent&gt;&gt;();

			A.CallTo(() =&gt; fakeEventHandler.Handle(A&lt;IEvent&gt;._))
				  .Invokes((IEvent ev) =&gt;
				  {
					  eventFromInvoke = ev;
				  });
		}

Na końcu konstruktora tworzymy fake dla metody Handler, tak by pobierać event jaki jest do niej przekazywany jako parametr, tak na zaś do assertów.

Pierwszy teścik taki symboliczny, czy faktycznie EventBus to EventBus prosto z AutoFac-a.

Test poprawności pobierania szyny zdarzeń z kontenera AutoFac.

1
2
3
4
5
6
7
8
9
10
[Fact]
public void test_event_bus_are_correct_resolved()
{
	using (var scope = container.BeginLifetimeScope())
	{
		var eventBus = scope.Resolve&lt;IEventBus&gt;();

		eventBus.ShouldBeOfType&lt;EventBus&gt;();
	}
}

Jak się powodzi to lecimy dalej.

A tutaj to sprawdzimy czy szyna zdarzeń po publikacji (Publish) oszukanego zdarzenia (fakeEvent) rzuci wyjąteczkiem TypeUnloadedException.

Próba publikacji zdarzenia, na pustą szynę, rzuca wyjątkiem.

1
2
3
4
5
6
7
8
9
10
11
12
[Fact]
public void event_bus_publish_should_throw_excetion()
{
	using (var scope = container.BeginLifetimeScope())
	{
		var eventBus = scope.Resolve&lt;IEventBus&gt;();

		var fakeEvent = A.Fake&lt;IEvent&gt;();

		Should.Throw&lt;TypeUnloadedException&gt;(() =&gt; { eventBus.Publish(fakeEvent); });
	}
}

A no bo POWINNA!

Teraz to już powinno być dobrze, pierwsza publikacja i odebranie prawidłowego IEvent, część kodu zawarta w TearUp, tak że tutaj prościutko.

Próba publikacji zdarzenia z wyrejestrowanie, powinna się powieść.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Fact]
public void event_bus_register_should_add_event_handler()
{
	using (var scope = container.BeginLifetimeScope())
	{
		var eventBus = scope.Resolve&lt;IEventBus&gt;();

		eventBus.Register(fakeEventHandler);
		eventBus.Publish(fakeEvent);
		eventBus.UnRegister(fakeEventHandler);

		eventFromInvoke.ShouldBeSameAs(fakeEvent);
	}
}

Odebrany IEvent musi być taki sam jak ten publikowany!

A tutaj sobie sprawdzimy jak działa Register i UnRegister.

Rzucanie wyjątkiem dla zarejestrowanego a następnie wyrejestrowanego hanldera.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void event_bus_register_and_unregister_should_throw_exception_when_publish()
{
    using (var scope = container.BeginLifetimeScope())
    {
        var eventBus = scope.Resolve&lt;IEventBus&gt;();
 
        eventBus.Register(fakeEventHandler);
        eventBus.UnRegister(fakeEventHandler);
 
        Should.Throw&lt;TypeUnloadedException&gt;(() =&gt; { eventBus.Publish(fakeEvent); });
        eventFromInvoke.ShouldBeNull();
    }
}

Zarejestrowany IEventHandler i wyrejestrowany po publikacji wali wyjątkiem TypeUnloadedException!

Ostatni tłusty teścik rejestruje aż 100 IEventHandlerów, po czym publikuje do nich fakeEvent-a.

Rejestrowanie 100 handlerów, i sprawdzanie czy otrzymują 100 prawidłowych zdarzeń.

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
[Fact]
public void event_bus_register_many_handlers_should_add_event_handler()
{
	using (var scope = container.BeginLifetimeScope())
	{
		var count = 100;
		var eventBus = scope.Resolve&lt;IEventBus&gt;();
		var eventHanlders = new List&lt;IEventHandler&lt;IEvent&gt;&gt;();
		var events = new List&lt;IEvent&gt;();

		for (var i = 0; i &lt; count; i++)
		{
			var eventHanlder = A.Fake&lt;IEventHandler&lt;IEvent&gt;&gt;();
			eventHanlders.Add(eventHanlder);

			A.CallTo(() =&gt; eventHanlder.Handle(A&lt;IEvent&gt;._))
				.Invokes((IEvent ev) =&gt;
				{
					events.Add(ev);
				});

			eventBus.Register(eventHanlder);
		}

		eventBus.Publish(fakeEvent);

		for (var i = 0; i &lt; count; i++)
		{
			eventBus.UnRegister(eventHanlders[i]);
		}

		foreach (var @event in events)
		{
			@event.ShouldBeSameAs(fakeEvent);
		}
	}
}

Tym samym powinno dojść 100 oszukanych eventów zapisanych do listy.

Na końcu to już wyrejestrowanie handlerków, i sprawdzenie czy faktycznie te 100 eventów jest oszukanych (fakeEvent).

Na koniec

CQRS Co prawda więcej w poście kodu niż treści, ale myślę, że zrozumiały tekst.

W kolejnym poście połączymy ES z CQRS, oraz dodamy zapowiadane na ten post walidatory.

Dziękuję za wytrwałość i zachęcam do komentowania.


Daj Się Poznać 2017

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
Facebook www.facebook.com/PictOgr-1729700930654225
Twitter twitter.com/gemu_gocom
RSS http://mrdev.pl/category/daj-sie-poznac-2017/feed

Zapisz się na listę :)