ASP.NET Core ile Event Sourcing - 01 Store

07.11.2022 | dakika okuma

1. Giriş

Bu örneği uygulamadan önce aşağıdaki yazıyı okumanızı öneririm.

Event Sourcing Neydi?
Önceden servislerimiz aşağıdaki gibi olurdu. Bu servisimiz yüzlerce satır koddan oluşurdu. Bu karmaşadan kurtulmak için CQRS ile servislerimizi “Command” ve “Query” olmak üzere ikiye böldük.…

Yukarıda belirttiğim yazıda aşağıdaki gibi cümle kurmuştum.

Bu yazıda Event Store'un store kısmı ile ilgileneceğiz. Yani event'leri kaydedebileceğimiz veritabanı özelliği ile ilgileneceğiz. Sonraki yazımda ise messaging kısmı ile ilgileneceğiz.

Örnek uygulama olarak klasik Kanban Board örneğini seçtim.

RESTful API endpoint’lerimiz aşağıdaki gibi olacak.

[POST] api/tasks/{id}/create
[PATCH] api/tasks/{id}/assign
[PATCH] api/tasks/{id}/move
[PATCH] api/tasks/{id}/complete

Event'lerimiz aşağıdaki gibi olacak.

CreatedTask : Task oluşturulduğunda bu isimde bir event oluşturulacak.
AssignedTask : Task birisine atandığında bu isimde bir event oluşturulacak.
MovedTask : Task bir bölüme(In Progress, Done) taşındığında bu isimde bir event oluşturulacak.
CompletedTask : Task tamamlandığında bu isimde bir event oluşturulacak.

2. Event Store'un Yüklenmesi

Aşağıdaki komut satırı ile Docker'da Event Store ayağa kaldırıyoruz.

    docker run -d --name eventstore -p 2113:2113 -p 1113:1113 eventstore/eventstore

Event Store ayağa kalktığında aşağıdaki adresten panele girebilirsiniz. Varsayılan kullanıcı adı "admin", parola ise "changeit".

http://localhost:2113/

3. API Projesinin Oluşturulması

"EventSourcingTaskApp" isminde ASP.NET Core Web API Application projesi oluşturuyoruz. Aşağıdaki komut satırı ile "EventStore.Client" paketini projeye yüklüyoruz.

    dotnet add package EventStore.Client -v 5.0.6

appsettings.json'a Event Store bağlantı bilgilerini aşağıdaki gibi ekliyoruz.

Startup.cs dosyasında ise Event Store ile bağlantı kuruyoruz ve DI Container'e ekliyoruz.

4. Temel Aggregate Sınıfı ve Aggregate Repository

Projeye "Core" ve "Infrastructure" isminde iki tane klasör ekleyelim. Core klasörüne Task ile ilgili entity'leri, event'leri ve exception'ları ekleyeceğiz. Infrastructure klasörüne ise Event Store ile iletişime geçecek olan repository sınıfımızı ekleyeceğiz.

4.1. Aggregate Base Class

Core klasörünün içerisinde "Framework" isminde bir klasör ekleyelim ve içerisine "Aggregate.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Bu standart bir class'tır. Aggregate'ler bu base class'dan türetilirler. Bizim örneğimizde Task bir aggregate ve bu base class'dan türetilecek.

9. satırda aggregate'e ait event'lerin depolanacağı değişkeni tanımlıyoruz.
11. satırda aggregate'in Id'si olacağını belirtiyoruz.
12. satırda aggregate'in varsayılan versiyonunun "-1" olduğunu belirtiyoruz.
16. satırda event'leri 9.satırda tanımlanan değişkene ekleyecek olan metotu yazıyoruz.
23. satırda event'leri aggregate'e uygulayacak olan metotu yazıyoruz. Event Store'dan okunan her event için bu metot çalıştırılarak aggregate'in son hali oluşturulacak.
33. satırda aggregate üzerindeki event'leri dönen metotu yazıyoruz. Event Store'a event'ler gönderilirken bu metot çalıştırılacak ve event'ler alınacak.

4.2. Aggregate Repository

Infrastructure klasörünün içerisine "AggregateRepository.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Bu da standart bir class'tır. Event Store'a event gönderirken veya Event Store'dan event'leri alırken bu repository'yi kullanıyoruz.

4.2.1. Event Gönderme (Append Events to Stream)

22. satırda aggregate üzerindeki event'leri alarak EventData class'ına mapliyoruz. Event Store, event'leri EventData tipinde saklar.

İlk parametre olarak event'in id'sini bekler.
İkinci parametre olarak event'in ismini bekler. Örnek event ismi; CreatedTask, AssignedTask vb.
Üçüncü parametre olarak event data'sının json tipinde mi olduğunu bekler.
Dördüncü parametre olarak event data'sını bekler. Byte array tipinde beklediği için serialize ve encoding işlemleri yapılıyor.
Beşinci parametre olarak metadata bekler. Bu parametre null geçilebilir fakat event class'ının tipini "fullname" olarak geçiriyoruz ki event'leri deserialize ederken bu class tipini kullanabilelim. Örnek fullname; EventSourcingTaskApp.Core.Events.CreatedTask.

36. satırda stream ismini belirtiyoruz. Event Store'da event'lerin kümesine stream adı verilir. Yani aggregate Event Store'da stream olarak ifade edilir. Örnek stream name; Task-518e7c15-36ac-4edb-8fa5-931fb8ffa3a5.

38. satırda Event Store'a event'ler kaydediliyor.

Örnek Stream
Örnek Stream
4.2.2. Event Okuma (Read Events from Stream)

47. satırda stream ismini belirtiyoruz.

53. satırda döngü içerisinde Event Store'dan event'ler versiyon numaralarına göre sırasıyla alınıyor.

58. satırda aggregate'in load metodu çalıştırılarak event'ler aggregate'e uygulanıyor ve aggregate'in son hali oluşturuluyor.

Startup.cs dosyasında AggregateRepository class'ını DI Container'e aşağıdaki gibi ekleyelim.

<strong>5. Task'in ve Use Case'lerin Tanımlanması</strong>

Bu bölümde Task ile ilgili event'leri, exception'ları, use case'leri oluşturmaya başlayacağız.

5.1. Task'in Tanımlanması

Core klasörünün içerisine "Task.cs" isminde bir aggregate class'ı ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Task'in başlığı (Title), bölümü (Section), kime atandığı (AssignedTo) ve tamamlandığı (IsCompleted) bilgisi olacak.

Yine Core içerisine "BoardSections.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

5.2. Exception'ların Tanımlanması

Core klasörünün içerisinde "Exceptions" isminde bir klasör ekleyelim ve içerisine "TaskAlreadyCreatedException.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım. Aynı id bilgisi ile task oluşturulmaya çalışılırsa bu hatayı fırlatacağız.

Yine Core klasörünün içerisindeki Exceptions klasörünün içerisine "TaskCompletedException.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım. Tamamlanan task üzerinde işlem yapılmaya çalışılırsa bu hatayı fırlatacağız.

Yine Core klasörünün içerisindeki Exceptions klasörünün içerisine "TaskNotFoundException.cs" isminde bir class ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım. Task bulunamazsa bu hatayı fırlatacağız.

Task aggregate'ini ve exception'ları oluşturduğumuza göre task ile ilgili use case'leri yazmaya başlayabiliriz.

5.3. Task Oluşturma (Create Task)

Task oluşturulduğunda; task'in id'sini, başlığını ve task'i kimin oluşturduğunu event data'sı olarak saklayacağız.

CreatedTask
CreatedTask

Core klasörünün içerisinde "Events" isminde bir klasör ekleyelim ve içerisine "CreatedTask.cs" isminde bir event class'ı ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Task.cs class'ını ise aşağıdaki şekilde düzenleyelim.

Create metotu ile CreatedTask event'ini aggregate üzerinde depoluyoruz ki bu depolanan event'leri Event Store'a gönderebilelim.
19. satırda ise Event Store'dan alınan CreatedTask event'ini aggregate'e uyguluyoruz.

5.4. Task Atama (Assign Task)

Task birisine atandığında; task'in id'sini, task'i kimin atadığını ve task'in kime atandığını event data'sı olarak saklayacağız.

AssignedTask
AssignedTask

Core klasörünün içerisindeki Events klasörüne "AssignedTask.cs" isminde bir event class'ı ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Task.cs class'ını aşağıdaki şekilde düzenleyelim.

Assign metotu ile AssignedTask event'ini aggregate üzerinde depoluyoruz ki bu depolanan event'leri Event Store'a gönderebilelim.
20. satırda ise Event Store'dan alınan AssignedTask event'ini aggregate'e uyguluyoruz.

5.5. Task Taşıma (Move Task)

Task "In Progress" veya "Done" bölümüne taşındığında; task'in id'sini, task'i kimin taşıdığını ve task'in hangi bölüme taşındığını event data'sı olarak saklayacağız.

MovedTask
MovedTask

Core klasörünün içerisindeki Events klasörüne "MovedTask.cs" isminde bir event class'ı ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Task.cs class'ını aşağıdaki şekilde düzenleyelim.

Move metotu ile MovedTask event'ini aggregate üzerinde depoluyoruz ki bu depolanan event'leri Event Store'a gönderebilelim.
21. satırda ise Event Store'dan alınan MovedTask event'ini aggregate'e uyguluyoruz.

5.6. Task Tamamlama (Complete Task)

Task tamamlandığında; task'in id'sini, task'i kimin tamamladığını event data'sı olarak saklayacağız.

CompletedTask
CompletedTask

Core klasörünün içerisindeki Events klasörüne "CompletedTask.cs" isminde bir event class'ı ekleyelim. Class'ın içerisine aşağıdaki kodu yapıştıralım.

Task.cs class'ını aşağıdaki şekilde düzenleyelim.

Complete metotu ile CompletedTask event'ini aggregate üzerinde depoluyoruz ki bu depolanan event'leri Event Store'a gönderebilelim.
22. satırda ise Event Store'dan alınan CompletedTask event'ini aggregate'e uyguluyoruz.

6. API Endpoint'lerinin Hazırlanması

"TasksController" isminde bir controller oluşturalım ve aşağıdaki kodu yapıştıralım.

Action'larda da göreceğiniz üzere önce AggregateRepository'nin Load metotu ile Event Store'dan event'ler alınıyor ve aggregate oluşturuluyor. Daha sonra aggregate üzerindeki use case metotları ile yeni event'ler aggregate'de deploanıyor. AggregateRepository'nin Save metotu ile bu depolanan event'ler Event Store'a gönderiliyor.

API'yi çalıştıralım ve API'ye istekler atalım.

Aşağıdaki curl komut satırı ile task oluşturalım.

    curl -d "title=Event Store kurulacak" -H "Content-Type: application/x-www-form-urlencoded" -X POST https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/create

Aşağıdaki curl komut satırı ile task'i birisine atayalım.

    curl -d "assignedTo=Aziz CETİN" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/assign

Aşağıdaki curl komut satırı ile task'i In-Progress'e çekelim.

    curl -d "section=2" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move

Aşağıdaki curl komut satırı ile task'i Done'a çekelim.

    curl -d "section=3" -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/move

Aşağıdaki curl komut satırı ile task'i tamamlayalım.

    curl -d -H "Content-Type: application/x-www-form-urlencoded" -X PATCH https://localhost:44361/api/tasks/3a7daba9-872c-4f4d-8d6f-e9700d78c4f5/complete

Event Store paneline girip stream'i kontrol edebilirsiniz.

http://localhost:2113/web/index.html#/streams/Task-3a7daba9-872c-4f4d-8d6f-e9700d78c4f5

Projenin son haline Github’dan erişebilirsiniz.

ahmetkucukoglu/aspnetcore-event-sourcing

ASP.NET Core ile Event Sourcing

C#
24
10

Kolay gelsin.

Yazıyı Paylaş

Yorumlar

2 yıl önce
Ahmet Bey detaylı örneğiniz için teşekkür ederim.

Ratelimit hatasınıda Gençay bey bloğunda çözümlemiş. ilgili arkadaşlar gençcay beyin bloğundan faydalanabilirler.

Devamını takip etmekteyim.
Yanıtla
3 yıl önce
Hocam öncelikle ellerinize sağlık.

Tüm çalışmaları yapmama rağmen aşağıdaki hatayı almaktayım. Bu konudaki fikrinizi öğrenebilir miyim?

EventStore.ClientAPI.Exceptions.RetriesLimitReachedException: Item Operation ReadStreamEventsForwardOperation (ee04b522-e8fd-4856-a305-618e083e3159): Stream: Task-3a7daba9-872c-4f4d-8d6f-e9700d78c4f5, FromEventNumber: 100, MaxCount: 4096, ResolveLinkTos: False, RequireLeader: True, retry count: 10, created: 09:05:28.832, last updated: 09:06:25.635 reached retries limit : 10
Yanıtla
Abdullah
1 yıl önce
Merhaba, yazılarınızı zevkle okuyorum lakin kod blokları görünmüyor. Düzeltme şansınız var mı?
Yanıtla
1 yıl önce
Merhaba,

Bilgilendirme için teşekkür ederim. Güncelleme yaptım.
Yanıtla
1 yıl önce
Asıl ben teşekkür ederim.
Yanıtla

Yorum bırak

Yanıtla

Yanıtlamayı iptal et
Bu site reCAPTCHA tarafından korunmaktadır ve Google Gizlilik Politikası ve Hizmet Şartları geçerlidir. Yorumunuz başarılı şekilde gönderildi reCaptcha doğrulanamadı
Muhabbetle ASP.NET Core ile geliştirildi.