Webhook Nedir?

İki farklı uygulama veya sistem arasında gerçek zamanlı iletişim sağlayan bir mekanizmadır. Genellikle bir web uygulamasının diğerine otomatik olarak bilgi göndermesi veya işlem yapması gerektiğinde kullanılır. Bir uygulamada belirli bir olay meydana geldiğinde, örneğin bir form doldurulduğunda veya bir ödeme işlemi tamamlandığında, bu olaya ilişkin bilgileri içeren bir HTTP isteği, webhook aracılığıyla başka bir uygulamaya gönderilir.

Webhook’lar sayesinde ilgili endpointlere istek atılarak gerçekleştirilen kontrollere gerek kalmaz. Bu sayede sunucu tarafı gereksiz yükten kurtulmuş olur. Ayrıca Data Consistency (Veri Tutarlılığı) ‘de sağlanmış olur.

Webhook’lar için aşağıdaki senaryoları örnek olarak verebiliriz:

  1. Ödeme işlemi tamamlandığında müşteriye otomatik bir e-posta gönderme.
  2. Bir blog gönderisi yayınlandığında sosyal medya platformlarında otomatik olarak paylaşım yapma.
  3. Bir müşteri destek bileti oluşturulduğunda destek ekibine bildirim gönderme.
  4. Bir siparişte güncelleme yapıldığında ilgili yerlere bildirim gönderme.

Webhook’lar, bir uygulamadan diğerine gerçek zamanlı veri iletimi sağladıkları için iş süreçlerini otomatikleştirmek ve entegrasyonları kolaylaştırmak için oldukça kullanışlıdır.

Biz örnek senaryo için yukarıda bahsettiğimiz senaryolardan en sonuncusunu uygulayabiliriz. İlk önce boş bir solution açalım ve içerisine 2 adet web api projesi ekleyelim. Ben isimlendirmelerimi aşağıdaki gibi yaptım.

Order.Api : Sipariş güncelleme, Subscribe olma ve Notification.Api servisini tetikleyen işlemleri içerecek. Bu apide RabbitMq ile ilgili işlemler yapacağımız için RabbitMQ.Client paketini nuget üzerinden yüklemeyi unutmayınız.

Notification.Api : Bildirim gönderecek servisimiz. Tabi bunu kodlamayacağız, tetiklendiğine dair ekrana mesaj basmamız yeterli.

Öncelikle Order.Api projemizde OrderController adında yeni bir controller oluşturalım.. Unutmayın burada sadece bir örnek çalışıyoruz bu işlemler farklı mimarilerde de uygulanabilir veya daha clean yazılabilir 🙂 Bağımlılıkları azaltmak ve daha esnek bir yapı elde etmek için bağımlılıkları interface ler aracılığıyla tanımlamak daha iyi olabilir. Bu, servislerin test edilebilirliğini artırır ve gelecekte başka bir mesajlaşma sistemi ya da farklı bir implemantasyon kullanmak istediğinizde kodu daha kolay değiştirebilmenizi sağlar.

C#
 [ApiController]
 [Route("api/[controller]")]
 public class OrderController : ControllerBase
 {
     private readonly RabbitMqPublisher _publisher;
     private readonly WebhookSubscriptionService _subscriptionService;

     public OrderController(RabbitMqPublisher publisher, WebhookSubscriptionService subscriptionService)
     {
         _publisher = publisher;
         _subscriptionService = subscriptionService;
     }

     [HttpPost("subscribe")]
     public IActionResult Subscribe([FromBody] string webhookUrl)
     {
         var subscription = new WebhookSubscription
         {
             WebhookUrl = webhookUrl
         };
         _subscriptionService.AddSubscription(subscription);
         return Ok(subscription);
     }

     [HttpPost("update")]
     public IActionResult UpdateOrder([FromBody] OrderUpdateRequest orderUpdateRequest)
     {
         var message = new OrderUpdateMessage
         {
             OrderId = orderUpdateRequest.OrderId,
             Status = orderUpdateRequest.Status,
             UpdatedDate = orderUpdateRequest.UpdatedDate
         };

         _publisher.PublishOrderUpdate(message);

         return Ok("Sipariş başarıyla güncellendi ve RabbitMq kuyruğuna gönderildi.");
     }
 }

Order.Api içerisinde Models klasörü altına :

C#
namespace Order.Api.Models
{
    public class OrderUpdateRequest
    {
        public int OrderId { get; set; }
        public string Status { get; set; }
        public DateTime UpdatedDate { get; set; }
    }
}
C#
namespace Order.Api.Models
{
    public class OrderUpdateMessage
    {
        public int OrderId { get; set; }
        public string Status { get; set; }
        public DateTime UpdatedDate { get; set; }
    }
}
C#
namespace Order.Api.Models
{
    public class WebhookSubscription
    {
        public Guid Id { get; set; } = Guid.NewGuid();
        public string WebhookUrl { get; set; }
        public DateTime SubscribedDate { get; set; } = DateTime.UtcNow;
    }
}

OrderUpdateRequest, OrderUpdateMessage, WebhookSubscription model / dto larını oluşturalım.

Şimdi gerekli servislerimizi yazalım. Öncelikle siparişte bir güncelleme olduğunda bu işlemi kuyruğa atacak RabbitMqPublisher class ımızı oluşturalım.

C#
using Order.Api.Models;
using RabbitMQ.Client;
using System.Text.Json;
using System.Text;

namespace Order.Api.Services
{
    public class RabbitMqPublisher
    {
        private readonly IConnection _connection;
        private string _queueName = "order_updated_queue";

        public RabbitMqPublisher()
        {
            var factory = new ConnectionFactory() { HostName = "localhost" };
            _connection = factory.CreateConnection();
        }

        public void PublishOrderUpdate(OrderUpdateMessage message)
        {
            using (var channel = _connection.CreateModel())
            {
                channel.QueueDeclare(queue: _queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);

                var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message));

                channel.BasicPublish(exchange: "", routingKey: _queueName, basicProperties: null, body: body);
            }
        }
    }
}

Subscribe ve Remove Subscribe işlemlerini içeren class ımız.

C#
using Order.Api.Models;

namespace Order.Api.Services
{
    public class WebhookSubscriptionService
    {
        private readonly List<WebhookSubscription> _subscriptions = new List<WebhookSubscription>();

        public IEnumerable<WebhookSubscription> GetAllSubscriptions()
        {
            return _subscriptions;
        }

        public void AddSubscription(WebhookSubscription subscription)
        {
            _subscriptions.Add(subscription);
        }

        public void RemoveSubscription(Guid id)
        {
            var subscription = _subscriptions.FirstOrDefault(s => s.Id == id);
            if (subscription != null)
            {
                _subscriptions.Remove(subscription);
            }
        }
    }
}

Subscribe olan clientların bilgisini DB de tutmamız daha doğru olacaktır fakat burada konuya odaklandığım için DB işlemlerini yapmayacağım. Dilerseniz siz geliştirirken o şekilde yapabilir, webhookurl dışında farklı bilgiler de tutabilirsiniz.

Son olarak kuyruğu dinleyen ve bir mesaj geldiğinde Subscribe olmuş clientları bilgilendirecek olan class ımızı yazalım.

C#
using Order.Api.Models;
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using System.Text.Json;
using System.Text;

namespace Order.Api.Services
{
    public class WebhookNotificationService
    {
        private readonly WebhookSubscriptionService _subscriptionService;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private string _queueName = "order_updated_queue";

        public WebhookNotificationService(WebhookSubscriptionService subscriptionService, IHttpClientFactory httpClientFactory)
        {
            _subscriptionService = subscriptionService;
            _httpClientFactory = httpClientFactory;

            var factory = new ConnectionFactory() { HostName = "localhost", Port = 5672 };
            _connection = factory.CreateConnection();
            _channel = _connection.CreateModel();

            _channel.QueueDeclare(queue: _queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
        }

        public void Start()
        {
            var consumer = new EventingBasicConsumer(_channel);
            consumer.Received += async (model, ea) =>
            {
                var body = ea.Body.ToArray();
                var message = JsonSerializer.Deserialize<OrderUpdateMessage>(Encoding.UTF8.GetString(body));

                if (message != null)
                {
                    await NotifySubscribers(message);
                }
            };

            _channel.BasicConsume(queue: _queueName, autoAck: true, consumer: consumer);
        }

        private async Task NotifySubscribers(OrderUpdateMessage message)
        {
            var client = _httpClientFactory.CreateClient();

            var webhookPayload = new
            {
                orderId = message.OrderId,
                status = message.Status,
                updatedDate = message.UpdatedDate
            };

            var jsonPayload = JsonSerializer.Serialize(webhookPayload);
            var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

            foreach (var subscription in _subscriptionService.GetAllSubscriptions())
            {
                await client.PostAsync(subscription.WebhookUrl, content);
            }
        }
    }
}

WebhookNotificationService imizin ayağa kalkması ve diğer injectionlar için Program.cs i aşağıdaki gibi düzenleyelim.

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddSingleton<RabbitMqPublisher>();
builder.Services.AddSingleton<WebhookSubscriptionService>();
builder.Services.AddSingleton<WebhookNotificationService>();

builder.Services.AddControllersWithViews();

var app = builder.Build();

// WebhookNotificationService'i başlat
var notificationService = app.Services.GetRequiredService<WebhookNotificationService>();
notificationService.Start();

Order.Api tarafındaki geliştirmelerimizi bitirdik.

Notification.Api tarafında ise NotificationController adında yeni bir controller açalım.

C#
using Microsoft.AspNetCore.Mvc;
using Notification.Api.Models;

namespace Notification.Api.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class NotificationController : Controller
    {
        private readonly ILogger<NotificationController> _logger;

        public NotificationController(ILogger<NotificationController> logger)
        {
            _logger = logger;
        }

        [Route("Send")]
        [HttpPost]
        public async Task<IActionResult> Send(OrderUpdateMessage message)
        {
            //Bildirim gönderim işlemleri.
            //await _notificationService.Send(message);

            Console.WriteLine(JsonConvert.SerializeObject(message));

            return Ok();
        }
    }
}

Order.Api aracılığı ile subscribe olurken Notification.Api mizin ayağa kalktığı url ve send metodunun tetiklenmesini istediğimiz için url imizi ona göre kayıt ettireceğiz. 2 uygulamada da gerekli geliştirmeleri tamamladıktan sonra teste geçebiliriz.

Test – Postman ekran görüntüleri:

2 projemizinde ayağa kalmasını istediğimiz için multiple startup ayarımızı, Solution>Properties dan yapıyoruz.

Projeleri ayağa kaldırdıktan sonra öncelikle Order.Api deki subscribe endpointine istek atıp, sipariş güncelleme işlemi yapıldığında bizi bilgilendirmesini istiyoruz.

Daha sonra sipariş güncelleme işlemi yapıyoruz.

RabbitMq da kuyruğumuz oluştu.

Ve Notification.Api mizin console ekranı:

Sonuç olarak; bu yazımızda temel bir webhook servisi oluşturmaya çalıştık. Proje kodlarına ve postman collection dosyasına buradan erişebilirsiniz.

Yeni yazılarda görüşmek üzere !

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Bir yanıt yazın

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