Mikroservis Mimarisinde Generic Email Consumer Yapısı Oluşturmak

Abdullah Öztürk
6 min readJun 28, 2024

--

Merhaba arkadaşlar,

Yakın zamanda mikro-monolitik olan projemi tamamen mikroservis mimarisine geçirdim ve bu süreçte çeşitli email template oluşturmam gerekiyordu. Bunu “Şifremi Unuttum, Hoşgeldin X kullanıcısı, Ödeme Başarılı, Ödeme Başarısız, Dekont, Şifre Sıfırlama” gibi başlıklar altında toplayabiliriz. Bu case karşısında olabildiğince generic bir yapı oluşturarak az kod, çok iş mantığını baz alarak Generic Email Consumer oluşturdum. Bu makalede ise size mikroservis mimarisinde böyle bir case karşısında nasıl bir çözüm sunabilirsiniz, bunu anlatacağım.

Yazımıza başlamadan önce hem proje altyapısını hem de projede kullanılan birkaç teknolojinin ne işe yaradığını kısaca anlatmak istiyorum.

Reflection Nedir?

Reflection, çalışma sırasındaki kodu incelememizi ve manipüle etmemizi sağlayan dahili bir .NET özelliğidir. Bu, bir türün meta verilerini (property’leri, fonksiyonları ve field’ları gibi) inceleyebileceğiniz ve bu türden nesneleri dinamik olarak oluşturabileceğiniz anlamına gelir.

Reflection kullanarak bir assembly içerisinde tüm tipleri elde edebilir ve bu tiplerin fonksiyonlarını, propertylerini ve diğer tüm elemanlarını inceleyebiliriz. Biz ise bize EmailTemplateModel tipinde gelen sınıfın tipini runtime sırasında öğrenip ona göre template oluştururken kullanacağız.

Masstransit Nedir?

.NET için geliştirilmiş olan, distributed uygulamaları rahatlıkla yönetebilmeyi ve çalıştırmayı amaçlayan, ücretsiz, open source bir Enterprise Service Bus Framework’üdür. Amazon SQS, RabbitMQ, Azure Service Bus ve ActiveMQ gibi birçok queue sistemini destekler. Masstransit hakkında daha fazla bilgi için Gençay Yıldız’ın ilgili yazısına bakabilirsiniz.

Mikroservis projemizde CommerceService’den, IdentityService’den ve PaymentService’den bize bazı email gönderimi için eventler fırlatılacak. Örnek eventler:
- SuccessPaymentEmailRequestEvent
- FailedPaymentEmailRequestEvent
- RegistrationEmailRequestEvent
- ForgotPasswordEmailRequestEvent
- SendInvoiceAsEmailRequestEvent
- ResetPasswordEmailRequestEvent

Email Service’deki GenericEmailConsumer’dan türemiş consumer sınıfları, email için belirlediğimiz kuyruğu dinleyip yukarıdaki sınıfları consume edecek. Bu sınıfları ilgili EmailModel sınıfına dönüştürdükten sonra ContentBuilder ve ResourceService aracılığıyla template’i oluşturacak sonrasında ilgili kişiye MailService ile mail gönderecek. Uygulama çalıştığı sürece bu işlemine devam edecek.

EmailService
├── Application
│ ├── Consumers
│ ├── Extensions
│ └── Services
│ ├── Abstract
│ ├── Concrete
│ │ ├── BaseService
│ │ ├── ContentBuilder
│ │ └── ResourceService
│ └── EmailWorker
├── Domain
│ └── EmailTemplateModel
│ ├── EmailRequest
│ ├── CreateInvoiceEmailTemplateModel
│ └── ...
├── Persistence
├── Configurations
├── Extensions
└── Files

Klasör yapımız bu şekilde olacaktır. Makalenin ilerleyen kısımlarında buraya tekrar değineceğiz. Öncesinde Shared katmanında oluşturmamız gereken birkaç attribute bulunuyor. Bu custom attribute’lere reflection ile EmailTemplateModelimizi dönerken ihtiyacımız olacak.

/// <summary>
/// Mark class as custom element. In this way, it can read attributes while html rendering
/// </summary>
public class CustomElementAttribute : Attribute { }

/// <summary>
/// Mark class as excluded element. In this way, it is excluded while html rendering
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ExcludeAttribute : Attribute { }

CustomElementAttribute, reflection ile bir tipin elemanları arasında dönülürken o elemanın property olmaması durumunda reflection tarafında kullanılacak GetValue fonksiyonunda patlamaması için oluşturulan bir attribute. Örnek olarak CreateInvoiceEmailTemplateModel sınıfımızda Transaction adlı bir sınıf tutuyoruz. Reflection ile bunu döndüğümüzde değeri olmayacağı için bize hata fırlatacak. Bunu engellemek amaçlı Transaction sınıfının üstüne bu attribute’ü ekliyoruz.

ExcludeAttribute, adından da anlaşılacağı üzere bir tipin belli elemanlarını hariç tutmak için kullandığımız bir attribute. Örnek olarak her EmailRequestModel sınıfında Body propertymiz bulunmakta lakin biz bunu html template render edip oluşturacağımız için bu property’i hariç tutabiliriz.

Proje biraz büyük olduğu için sadece iki tane email template üzerinden göstereceğim. Makale sonunda proje linkini paylaşacağım, oradan inceleyebilirsiniz.

Shared katmanında Events/Mail altında EmailRequestEvent ve ForgotPasswordEmailRequestEvent gibi birkaç sınıf oluşturacağız. Ek olarak Domain/Constants altında EmailType enum oluşturuyoruz.

public class EmailRequestEvent
{
public string To { get; set; }
}

public class ForgotPasswordEmailRequestEvent : EmailRequestEvent
{
public string FullName { get; set; }
public string ResetCode { get; set; }
}

public class SendInvoiceAsEmailRequestEvent : EmailRequestEvent
{
public long TransactionId { get; set; }
}

public enum EmailType
{
Registration = 1,
ForgotPassword = 2,
SuccessPayment = 3,
FailedPayment = 4,
Invoice = 5,
ResetPassword = 6,
}

EmailService projesine geri dönelim. Burada Domain/EmailTemplateModel altında hem base sınıfı oluşturalım hem de diğer TemplateModel sınıflarını bu sınıftan türetelim.

public class EmailRequest
{
public EmailRequest(EmailType emailType) {
EmailType = emailType;
}

[Exclude]
[Required]
public EmailType EmailType { get; private set; }

[Description("#EMAIL#")]
public string To { get; set; }

[Description("#SUBJECT#")]
public string Subject { get; set; }

[Exclude]
public string Body { get; set; }
}

Burada html template üzerindeki tüm değiştirilecek olan kısımları #EMAIL# gibi bir format kullanarak belirtiyoruz. String.Replace metotu ile ilgili alanı bulup kolaylıkla değiştirebiliyoruz. Ayrıca yukarıda bahsettiğim üzere EmailType ve Body’i biz belirleyeceğimiz için onlar exclude edildi.

public class ForgotPasswordEmailTemplateModel : EmailRequest
{
public ForgotPasswordEmailTemplateModel() : base(EmailType.ForgotPassword) { }

[Description("#FULL_NAME#")]
public string FullName { get; set; }

[Description("#RESET_CODE#")]
public string ResetCode { get; set; }
}
public class CreateInvoiceEmailTemplateModel : EmailRequest
{
public CreateInvoiceEmailTemplateModel() : base(EmailType.Invoice) { }

[CustomElement]
public Transaction Transaction { get; set; }
[CustomElement]
public Product Product { get; set; }
[CustomElement]
public Vendor Vendor { get; set; }
[CustomElement]
public Address Address { get; set; }
[CustomElement]
public Coupon Coupon { get; set; }
}

Şimdi Files klasöründe yukarıda ilgili event sınıflarının html templatelerini oluşturalım. Aşağıda ödemesi başarısız olan bir işlemin email template’i bulunmakta.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>#SUBJECT#</title>
<style type="text/css">
<!-- CSS Kodları -->
</style>
</head>
<body>
<div class="container">
<p style="font-weight: bold; font-size: 18px;">Reset Password</p>
<p>Dear #FULL_NAME#,</p>
<p>We received a request to reset your password. We've posted a code below for password reset valid for 10 minutes.</p>
<div align="center">
<h4>#RESET_CODE#</h4>
</div>
<p>If you did not request this reset, please ignore this email.</p>
</div>
</body>
</html>

Application katmanında Services/Concrete altında ResourceService oluşturalım ve bu servisten hem dosya yolunu çekebileceğimiz hem de yukarıda görmüş olduğunuz #SUBJECT# alanını çekebileceğimiz fonksiyonlar oluşturacağız.

public class ResourceService : IResourceService
{
public string GetContent(EmailType emailType)
{
var content = emailType switch
{
EmailType.Registration => File.ReadAllText("../../Files/RegistrationEmailTemplate.html"),
EmailType.ForgotPassword => File.ReadAllText("../../Files/ForgotPasswordEmailTemplate.html"),
EmailType.SuccessPayment => File.ReadAllText("../../Files/SuccessPaymentEmailTemplate.html"),
EmailType.FailedPayment => File.ReadAllText("../../Files/FailedPaymentEmailTemplate.html"),
EmailType.Invoice => File.ReadAllText("../../Files/InvoiceEmailTemplate.html"),
EmailType.ResetPassword => File.ReadAllText("../../Files/ResetPasswordEmailTemplate.html"),
};

return content;
}

public string GetSubject(EmailType emailType)
{
var subject = emailType switch
{
EmailType.Registration => "Welcome To Website!",
EmailType.ForgotPassword => "Website - Forgot Password!",
EmailType.SuccessPayment => "Website - Payment Successfull!",
EmailType.FailedPayment => "Website - Payment Failed!",
EmailType.Invoice => "Website - Invoice",
EmailType.ResetPassword => "Website - Password Reset Completed!",
};

return subject;
}
}

Bu kısmı daha dinamik yapabilirsiniz. Veritabanından yada başka bir kaynaktan çekebilirsiniz. Ben şuanlık en basit haliyle yazıp devam ediyorum. Sırada yazımızda önemli rol oynayan ContentBuilder sınıfımız var. Gelin, inceleyelim.

Build metotumuzun içine girdiğimizde yazdığımız kodları madde madde açıklayalım.

  • 13–14 satırlarında bize gelen sınıfın tipini öğrenip bu tipin elemanlarına yani propertylerine erişiyoruz.
  • 16. satırda yukarıda anlatmış olduğum resource servisteki GetContent metotunu çağırarak html template’i elde ediyoruz.
  • 18. satırdan itibaren tüm propertyleri teker teker döngüye soktuk.
  • 20–22 satırlarında ilgili property bu attributelere sahip mi diye kontrol sağlıyoruz.
  • 24. satırda eğer bu property ExcludeAttribute’üne sahipse yani hariç tutulmuşsa continue diyip diğer elemana geçiyoruz.
  • 27.satırda eğer CustomElementAttribute yoksa yani normal bir property ise bu koşulun içine giriyor, DescriptionAttribute kontrolü sağlıyoruz. Sonuçta template’de bir alan ifade ediyor olmalı. Gerekli koşullar sağlandıktan sonra property değeri templatedeki yerini alıyor.
  • 37–51 satırları arasında yukarıda bahsettiğimiz olayların aynısı yaşanıyor. Farklı olarak bu property CustomElement ile işaretlendiği için aslında bir sınıf. Onun da propertylerine erişip aynı işlemleri yapıyoruz. (Burayı sınıf özelinde geliştirdik ancak sizin ihtiyaçlarınıza bağlı olarak Factory Design Pattern kullanarak ilgili ValueProvider sınıfı oluşturabilirsiniz.)
  • Son olarak template’imizi oluşturduk, sadece #SUBJECT# kısmı kaldı. Onu da GenericEmailConsumer içerisinde çağıracağız.

Application/Consumers klasörüne gelip GenericEmailConsumer sınıfını oluşturuyoruz.

GenericEmailConsumer sınıfımızı IConsumer interface’inden türetip generic olarak 2 tane parametre alıyoruz. Bunlardan TEmailEvent bizim EmailRequestEvent’den türemiş sınıf, TEmailModel ise render için kullanacağımız Email Model. Şimdi Consume metotu içerisinde yaptıklarımızı madde madde açıklayalım.

  • Öncelikle 17.satırda bize gelen EmailRequestEvent sınıfını elde edelim. Sürekli Context.Message diye çağırmayalım.
  • 19. satırda bize gelen bu sınıfı gerekli EmailTemplateModel’e mapleyelim. Burada istediğiniz mapper aracını kullanabilirsiniz. Ben burada Mapster kullandım.
  • 21. satırda subject değerini alıp bunu 22.satırda parametre olarak kullanıyoruz. Artık elimizde bir html body var.
  • (İsteğe bağlı) Emaillerin gönderilmemesi yada kaybolması ihtimaline karşın bize gelen emailleri öncesinde veritabanına kaydediyor, sonrasında EmailService’i çağırıp mail gönderiyoruz. Mail gönderme işlemi başarısız olması durumunda geçerli kayıtı Failed olarak işaretliyoruz. Burada db’ye kaydetme kısmına değinmeyeceğim ancak projede bu bahsettiğim kısmı inceleyebilirsiniz.

Son olarak artık bu sınıfı türeteceğimiz sınıflar oluşturalım ve çıktıyı gösterip makaleyi sonlandıralım.

public class ResetPasswordEmailRequestConsumer : GenericEmailRequestConsumer<ResetPasswordEmailRequestEvent, ResetPasswordEmailTemplateModel>
{
public ResetPasswordEmailRequestConsumer(IContentBuilder contentBuilder, IResourceService resourceService) : base(contentBuilder, resourceService)
{

}
}

Bu kadar basit bir şekilde implemente edebiliyoruz. Rabbitmq ile mesaj gönderme kısmına girmeyeceğim lakin gönderme sonrasında emailimize gelen çıktıyı sizlere göstermek istiyorum.

Şifre Sıfırlama Emaili
Başarılı Ödeme Emaili

Bu yazıda custom attribute oluşturup nasıl html template render edebileceğinizi, masstransit altyapısıyla daima çalışan bir background service oluşturup reflection ile render işlemi yaparak generic email consumer yapısı nasıl oluşturulacağı ile alakalı sorunlara kendimce çözüm üreterek sizlerle paylaştım.

Okuduğunuz için teşekkür ederim. Faydalı bulduysanız beğenilerinizi eksik görmeyin. Bir sonraki yazıda görüşmek dileğiyle.

--

--