Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Basit Bir Event Sourcing Uygulaması Geliştirelim

Merhaba,

Bu içeriğimizde bir önceki kaleme aldığımız Event Sourcing Nedir? Haydi Gelin Hep Beraber İnceleyelim başlıklı makalemize basit bir somut örnek teşkil edecek şekilde çalışma gerçekleştireceğiz. Amacımız Event Sourcing’in pratikte ucundan kıyısından tadına bakmak ve konuya dair sonradan kaleme alacağımız Event Store gibi bir tool’un şimdiden kıymetini anlamak 🙂 Evet… O halde hazırsanız başlayalım…

Kurgu

İlk olarak yapacağımız çalışmaya dair genel kurgudan bahsetmek istiyorum. Örneklendirmemiz bir console uygulaması üzerinde var olan ‘Person’ nesnesine dair ‘Command’ ve ‘Query’lerin ayrımı üzerinden seyredecektir. Haliyle buradan anlaşılıyor ki, temel CQRS pattern eşliğinde basit bir Event Sourcing yaklaşımı sergileyeceğiz.

    record Person
    {
        public int Id { get; init; }
        public string Name { get; private set; }
        public int Age { get; private set; }

        public Person(int id, string name, int age)
        {
            Id = id;
            Name = name;
            Age = age;
        }

        public void ChangeName(string name)
        => Name = name;
        public void ChangeAge(int age)
        => Age = age;
    }

    class PersonSource
    {
        public static List<Person> PersonList { get; } = new()
        {
            new(1, "Gençay", 28),
            new(2, "Hilmi", 30),
            new(3, "Coşgun", 42),
            new(4, "Rıfkı", 32)
        };
    }

CQRS Temelinin Oluşturulması

Öncelikle CQRS temelini atarak uygulamayı inşa etmeye başlayabiliriz.
Query
Request nesneleri:

    class GetPersonQueryRequest
    {
        public int Id { get; set; }
    }
    class GetAllPersonQueryRequest
    {

    }

Response nesneleri:

    class GetPersonQueryResponse
    {
        public Person Person;
    }
    class GetAllPersonQueryResponse
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

Handler nesneleri:

    class GetPersonQueryHandler
    {
        public GetPersonQueryResponse GetPerson(GetPersonQueryRequest request)
        {
            Person person = PersonSource.PersonList.FirstOrDefault(p => p.Id == request.Id);
            return new GetPersonQueryResponse
            {
                Person = person
            };
        }
    }
    class GetAllPersonQueryHandler
    {
        public List<GetAllPersonQueryResponse> GetAll(GetAllPersonQueryRequest request)
        {
            return PersonSource.PersonList.Select(person => new GetAllPersonQueryResponse
            {
                Id = person.Id,
                Age = person.Age,
                Name = person.Name
            }).ToList();
        }
    }

Command
Request nesneleri:

    class ChangeNamePersonCommandRequest
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    class ChangeAgePersonCommandRequest
    {
        public int Id { get; set; }
        public string Age { get; set; }
    }
    class CreatePersonCommandRequest
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

Response nesneleri:

    class ChangeNamePersonCommandResponse
    {
        public bool Register = true;
    }
    class ChangeAgePersonCommandResponse
    {
        public bool Register = true;
    }
    class CreatePersonCommandResponse
    {
        public Person Person { get; set; }
    }

Handler nesneleri:

    class ChangeNamePersonCommandHandler
    {
        public ChangeNamePersonCommandResponse ChangeName(ChangeNamePersonCommandRequest request)
        {
            Person person = PersonSource.PersonList.FirstOrDefault(p => p.Id == request.Id);
            person.ChangeName(request.Name);
            return new ChangeNamePersonCommandResponse
            {
                Register = true
            };
        }
    }
        public ChangeAgePersonCommandResponse ChangeAge(ChangeAgePersonCommandRequest request)
        {
            Person person = PersonSource.PersonList.FirstOrDefault(p => p.Id == request.Id);
            person.ChangeAge(request.Age);
            return new ChangeAgePersonCommandResponse
            {
                Register = true
            };
        }
    class CreatePersonCommandHandler
    {
        public CreatePersonCommandResponse CreatePerson(CreatePersonCommandRequest request)
        {
            PersonSource.PersonList.Add(new Person(request.Id, request.Name, request.Age));
            return new CreatePersonCommandResponse
            {
                Person = PersonSource.PersonList.Last()
            };
        }
    }

Evet… Görüldüğü üzere CQRS pattern’a uygun bir şekilde gelecek istekleri ayırmış olduk. Bundan sonra herhangi bir ‘Person’ nesnesine yapılan ‘Command’ isteklerini event olarak tutabilmemiz için öncelikle ‘Event’ tasarımlarında bulunmamamız gerekmektedir.

Event Tasarımlarını Gerçekleştirmek

    interface IEvent
    {
    }
    class NameChangedEvent : IEvent
    {
        public Person Person { get; set; }
        public string oldName, newName;
        public NameChangedEvent(Person person, string oldName, string newName)
        {
            Person = person;
            this.oldName = oldName;
            this.newName = newName;
        }
        public override string ToString()
        => $"Id değeri {Person.Id} olan personelin adı değiştirilmiştir.\nEski adı : {oldName}\nYeni adı : {newName}";
    }

Yukarıdaki kod bloğunu incelerseniz eğer bir ‘Person’ nesnesinin ‘Name’ değerinin değiştiğini ifade eden ‘NameChangedEvent’ isminde bir sınıf tasarlanmıştır. Aynı şekilde ‘Age’ değerinin değiştiğini ve yeni bir ‘Person’ eklendiğini ifade eden ‘AgeChangedEvent’ ve ‘PersonCreatedEvent’ isimli event sınıfları da aşağıdadır.

    class AgeChangedEvent : IEvent
    {
        public Person Person { get; set; }
        public int oldAge, newAge;

        public AgeChangedEvent(Person person, int oldAge, int newAge)
        {
            Person = person;
            this.oldAge = oldAge;
            this.newAge = newAge;
        }
        public override string ToString()
            => $"Id değeri {Person.Id} olan personelin yaşı değiştirilmiştir.\nEski yaş : {oldAge}\nYeni yaş : {oldAge}";
    }
    class PersonCreatedEvent : IEvent
    {
        public Person Person { get; set; }

        public PersonCreatedEvent(Person person)
        {
            Person = person;
        }

        public override string ToString()
        => $"Yeni personel {Person.Id} id değerinde eklenmiştir.";
    }

Event Broker Tasarımını Gerçekleştirmek

Ve son olarak ‘Person’ nesnesi üzerinde yaratılacak olan tüm event’leri toplayacak olan ve gerekli aksiyonları alacak olan ‘Event Broker’ nesnesini oluşturmamız gerekmektedir.

    class EventBroker
    {
        //Oluşan event'leri depolayacağımız koleksiyon.
        public List<IEvent> allEvents = new();
        //Uygulamada gelen Command isteklerinde devreye girecek event tanımlanıyor.
        public event EventHandler<object> Commands;
        //Uygulamada gelen Query isteklerinde devreye girecek event tanımlanıyor.
        public event EventHandler<object> Queries;
        public EventBroker()
        {
            //'Commands' isimli 'EventHandler'a aşağıdaki metot tanımlanıyor.
            this.Commands += (sender, command) =>
            {
                //Eğer gelen Command 'ChangeAgePersonCommandRequest' türündense
                if (command is ChangeAgePersonCommandRequest req1)
                {
                    ChangeAgePersonCommandHandler handler = new ChangeAgePersonCommandHandler();
                    ChangeAgePersonCommandResponse response = handler.ChangeAge(req1);
                    //İlgili isteğe uygun event olan 'AgeChangedEvent' nesnesi event deposuna atılıyor.
                    this.allEvents.Add(new AgeChangedEvent(response.Person, response.OldAge, req1.Age));
                }
                //Eğer gelen Command 'ChangeNamePersonCommandRequest' türündense
                else if (command is ChangeNamePersonCommandRequest req2)
                {
                    ChangeNamePersonCommandHandler handler = new ChangeNamePersonCommandHandler();
                    ChangeNamePersonCommandResponse response = handler.ChangeName(req2);
                    //İlgili isteğe uygun event olan 'NameChangedEvent' nesnesi event deposuna atılıyor.
                    this.allEvents.Add(new NameChangedEvent(response.Person, response.OldName, req2.Name));
                }
                //Eğer gelen Command 'CreatePersonCommandRequest' türündense
                else if (command is CreatePersonCommandRequest req3)
                {
                    CreatePersonCommandHandler handler = new CreatePersonCommandHandler();
                    CreatePersonCommandResponse response = handler.CreatePerson(req3);
                    //İlgili isteğe uygun event olan 'PersonCreatedEvent' nesnesi event deposuna atılıyor.
                    this.allEvents.Add(new PersonCreatedEvent(response.Person));
                }
            };
            //'Queries' isimli 'EventHandler'a aşağıdaki metot tanımlanıyor.
            this.Queries += (sender, query) =>
            {
                //Eğer gelen Query 'GetPersonQueryRequest' türündense
                if (query is GetPersonQueryRequest req1)
                {
                    GetPersonQueryHandler handler = new GetPersonQueryHandler();
                    GetPersonQueryResponse response = handler.GetPerson(req1);
                    Console.WriteLine($"Id\tName\tAge");
                    Console.WriteLine($"{response.Person.Id}\t{response.Person.Name}\t{response.Person.Age}\n***********");
                }
                //Eğer gelen Query 'GetAllPersonQueryRequest' türündense
                else if (query is GetAllPersonQueryRequest req2)
                {
                    GetAllPersonQueryHandler handler = new GetAllPersonQueryHandler();
                    List<GetAllPersonQueryResponse> response = handler.GetAll(req2);
                    Console.WriteLine($"Id\tName\tAge");
                    response.ForEach(person => Console.WriteLine($"{person.Id}\t{person.Name}\t{person.Age}"));
                    Console.WriteLine("***********");
                }
            };
        }
        //Client'ın 'Command' gönderebilmesi için kullanacağı metot.
        public void Command(object command)
            => Commands(this, command);
        //Client'ın 'Query' gönderebilmesi için kullanacağı metot.
        public void Query(object query)
            => Queries(this, query);
        //Tüm event'i yazdırabilmek/okuyabilmek için kullanılacak metot.
        public void WriteAllEvent()
            => allEvents.ForEach(@event => Console.WriteLine($"{@event.ToString()}\n***********"));
    }

Yukarıdaki kod bloğunu incelerseniz temel düzeyde bir ‘Event Broker’ nesnesinde olması gereken tüm aksiyonlar tasarlanmıştır. Adım adım kod satırlarında gerekli açıklamaları yaptığım için burada tekrardan detaylandırmaya girmek istemesem de genel anlamda bir izahatte bulunmanın faydası olacağı kanaatindeyim: Dikkat ederseniz uygulamadaki tüm event’ler, yapılan ‘Command’ ve ‘Query’ isteklerine istinaden gerekli olaylar eşliğinde, isteğin türüne göre ayrıştırma gerçekleştirildikten sonra ‘event’ olarak kayıt altına alınmaktadır. Ardından bu event’ler istek doğrultusunda tekrar listelenebilmekte ve hatta eldeki verinin herhangi bir T zamanındaki halini elde edebilecek vaziyette geliştirilmeye meyletmektedir.

Dolayısıyla şu aşamadan sonra artık tek yapılması gereken client’ı tasarlamak ve test amaçlı bir kaç ‘Command’ yahut ‘Query’ isteklerini göndermektir.

Client

    class Program
    {
        static void Main(string[] args)
        {
            EventBroker eventBroker = new EventBroker();
            eventBroker.Command(new CreatePersonCommandRequest { Id = 5, Age = 21, Name = "Muiddin" });
            eventBroker.Command(new CreatePersonCommandRequest { Id = 6, Age = 44, Name = "Fadiş" });
            eventBroker.Command(new ChangeAgePersonCommandRequest { Id = 2, Age = 55 });
            eventBroker.Command(new ChangeAgePersonCommandRequest { Id = 2, Age = 66 });
            eventBroker.Command(new ChangeNamePersonCommandRequest { Id = 3, Name = "Serdar" });
            eventBroker.WriteAllEvent();
            eventBroker.Query(new GetAllPersonQueryRequest());
            eventBroker.Query(new GetPersonQueryRequest() { Id = 3 });
        }
    }

Görüldüğü üzere client ‘CreatePersonCommandRequest‘, ‘CreatePersonCommandRequest‘, ‘ChangeAgePersonCommandRequest‘, ‘ChangeAgePersonCommandRequest‘ ve ‘ChangeNamePersonCommandRequest‘ olmak üzere beş adet Command isteğinde bulunmakta ve akabinde ‘GetAllPersonQueryRequest‘ ve ‘GetPersonQueryRequest‘ olmak üzere iki Query isteğiyle verileri okumak istemektedir. 11. satırda ise ‘WriteAllEvent’ fonksiyonu ile o ana kadar oluşan tüm event’leri listelemektedir. Sonuç olarak aşağıdaki gibi bir çıktıyla karşılaşılmaktadır.
Basit Bir Event Sourcing Uygulaması Geliştirelim

Evet… Bu satırlara geldiysek eğer artık hasbel kader bir Event Sourcing operasyonunun nasıl ceyran ettiğini anlamış, hiç yoktan tadına bakmış bulunmaktayız. Ve umuyorum ki, yoğun uygulamalarda buradaki gibi event’leri in-memory’de depolamanın maliyetinin ağırlığını ve yönetilebilirliğinin güçlüğünü gördüğünüz kanaatindeyim. Haliyle event’leri güçlü bir şekilde depolayabilmenin ve yönetebilmenin kıymetinin önemine binaen sonraki yazımda Event Sourcing için oldukça kullanışlı bir tools olan Event Store‘la sizleri tanıştırmak istiyorum.

O halde sizler bu makaleyi okuyup sindirene kadar ben de müjdesini verdiğim diğer makaleyi klavyeye almaya başlasam iyi olacak 🙂

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek projeyi indirmek için buraya tıklayınız.

Bunlar da hoşunuza gidebilir...

1 Cevap

  1. 09 Mayıs 2021

    […] önceki Basit Bir Event Sourcing Uygulaması Geliştirelim başlıklı makalemizde Event Sourcing pattern’ının nasıl gerçekleştirilebildiğine […]

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir