.NET Core’da Idempotency (Attribute-Tabanlı) : Dinamik, Esnek ve In-Memory Çözüm

Günümüzün dağıtık ve mikroservis tabanlı mimarilerinde, bir işlemin veya isteğin aynı parametrelerle birden çok kez çalıştırılması çoğu zaman istem dışı yan etkilere (duplicate kayıtlar, çift faturalar, tekrar eden ödemeler) yol açabilir. Bu gibi senaryolarda idempotency kavramı kritik bir rol oynar.

Matematikte idempotensi, bir operatörün üst üste uygulandığında sonucun değişmemesi anlamına gelir. Yazılım dünyasında ise aynı isteğin tekrar tekrar gönderilmesi, sistemde sadece bir kez gerçekleşmiş gibi işlenmesi hedeflenir. Özellikle HTTP üzerinden yapılan POST, PUT veya DELETE gibi işlemlerde, ağ kesintisi veya zaman aşımı durumlarına karşı güvenilir retry mekanizmalarının kurulabilmesi için idempotency büyük önem taşır.

Bu makalede, .NET Core API’lerinizde attribute tabanlı idempotency özelliğini nasıl dinamik, esnek ve in-memory bir çözümle uygulayabileceğinizi adım adım göstereceğiz.

Neden Idempotency?

  • Tekrarlanan İsteklerin Güvenilir Yönetimi
    Ağ hatalarından veya client retry mekanizmalarından kaynaklı tekrar eden istekler, idempotency sayesinde aynı sonucu üretir; böylece duplicate veri oluşturulmaz.
  • Veri Tutarlılığı ve Bütünlüğü
    Ödeme, sipariş veya biletleme gibi finansal süreçlerde çift kayıtlar, finansal mutabakat süreçlerinde ciddi uyumsuzluklara yol açabilir. Idempotency ile, aynı Idempotency-Key ile gelen ikinci istek, önceden kaydedilmiş yanıtı döndürerek sistemde tek bir kayıt oluşturulmasını sağlar.
  • Esneklik ve Sınırlı Kapsam
    Sadece kritik endpoint’lerde idempotency uygulayarak global middleware yükünden kaçınabilir, ihtiyacınız olan noktalarda minimal ek yükle güvenliği sağlayabilirsiniz.

Çözüm: Attribute-Tabanlı Idempotency Filter

1. IdempotentAttribute

Aşağıdaki attribute, herhangi bir controller aksiyonu veya sınıfına eklenerek idempotency’i aktifleştirmenize olanak tanır:

[AttributeUsage(AttributeTargets.Method)]
public class IdempotentAttribute : Attribute
{
    public string HeaderName { get; set; } = "Idempotency-Key";
    public int TtlSeconds { get; set; } = 300;
}
  • HeaderName: Default olarak Idempotency-Key kullanılır. İstendiğinde parametre ile değiştirilebilir.
  • TtlSeconds: Default 300 saniye. Attribute parametresi ile özelleştirilebilir.

2. In-Memory Store: MemoryCacheIdempotencyStore

.NET Core’un IMemoryCache mekanizmasını kullanarak yanıtları geçici bellekte saklayan basit store:

public class MemoryCacheIdempotencyStore : IIdempotencyStore
{
    private readonly IMemoryCache _memoryCache;
    private readonly ILogger<MemoryCacheIdempotencyStore> _logger;

    public MemoryCacheIdempotencyStore(IMemoryCache memoryCache, ILogger<MemoryCacheIdempotencyStore> logger)
    {
        _memoryCache = memoryCache;
        _logger = logger;
    }

    public Task<IdempotencyRecord?> GetAsync(string key)
    {
        try
        {
            var record = _memoryCache.Get<IdempotencyRecord>(key);
            _logger.LogDebug("Retrieved idempotency record for key: {Key}, Found: {Found}", key, record != null);
            return Task.FromResult(record);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving idempotency record for key: {Key}", key);
            return Task.FromResult<IdempotencyRecord?>(null);
        }
    }

    public Task SaveAsync(IdempotencyRecord record, TimeSpan ttl)
    {
        try
        {
            var options = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = ttl,
                Priority = CacheItemPriority.Normal
            };

            _memoryCache.Set(record.Key, record, options);
            _logger.LogDebug("Saved idempotency record for key: {Key}, TTL: {TTL}", record.Key, ttl);
            
            return Task.CompletedTask;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error saving idempotency record for key: {Key}", record.Key);
            return Task.CompletedTask;
        }
    }
}

public class IdempotencyRecord
{
    public string Key { get; set; } = string.Empty;
    public int StatusCode { get; set; }
    public string ResponseBody { get; set; } = string.Empty;
    public string ContentType { get; set; } = "application/json";
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

3. Action Filter: IdempotencyFilter

Gerçek iş akışı öncesi ve sonrası cache kontrolü yaparak duplicate istekleri engelleyen filtre:

public class IdempotencyFilter : ActionFilterAttribute
{
    private readonly IIdempotencyStore _store;
    private readonly ILogger<IdempotencyFilter> _logger;

    public IdempotencyFilter(IIdempotencyStore store, ILogger<IdempotencyFilter> logger)
    {
        _store = store;
        _logger = logger;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // IdempotentAttribute var mı kontrol et
        var idempotentAttribute = context.ActionDescriptor.EndpointMetadata
            .OfType<IdempotentAttribute>()
            .FirstOrDefault();

        if (idempotentAttribute == null)
        {
            await next();
            return;
        }

        // Idempotency key'i al
        var idempotencyKey = context.HttpContext.Request.Headers[idempotentAttribute.HeaderName].FirstOrDefault();
        
        if (string.IsNullOrEmpty(idempotencyKey))
        {
            context.Result = new BadRequestObjectResult(new 
            { 
                error = $"Missing required header: {idempotentAttribute.HeaderName}" 
            });
            return;
        }

        _logger.LogDebug("Processing idempotency key: {Key}", idempotencyKey);

        // Cache'den varolan kaydı kontrol et
        var existingRecord = await _store.GetAsync(idempotencyKey);
        if (existingRecord != null)
        {
            _logger.LogInformation("Returning cached response for idempotency key: {Key}", idempotencyKey);
            
            context.Result = new ContentResult
            {
                StatusCode = existingRecord.StatusCode,
                Content = existingRecord.ResponseBody,
                ContentType = existingRecord.ContentType
            };
            return;
        }

        // Action'ı çalıştır
        var executedContext = await next();

        // Başarılı response'u cache'le
        if (executedContext.Result is ObjectResult objectResult && objectResult.StatusCode >= 200 && objectResult.StatusCode < 300)
        {
            var responseBody = JsonSerializer.Serialize(objectResult.Value);
            var record = new IdempotencyRecord
            {
                Key = idempotencyKey,
                StatusCode = objectResult.StatusCode ?? 200,
                ResponseBody = responseBody,
                ContentType = "application/json"
            };

            var ttl = TimeSpan.FromSeconds(idempotentAttribute.TtlSeconds);
            await _store.SaveAsync(record, ttl);
            
            _logger.LogInformation("Cached response for idempotency key: {Key}, TTL: {TTL}", idempotencyKey, ttl);
        }
        else if (executedContext.Result is ContentResult contentResult && contentResult.StatusCode >= 200 && contentResult.StatusCode < 300)
        {
            var record = new IdempotencyRecord
            {
                Key = idempotencyKey,
                StatusCode = contentResult.StatusCode ?? 200,
                ResponseBody = contentResult.Content ?? string.Empty,
                ContentType = contentResult.ContentType ?? "application/json"
            };

            var ttl = TimeSpan.FromSeconds(idempotentAttribute.TtlSeconds);
            await _store.SaveAsync(record, ttl);
            
            _logger.LogInformation("Cached response for idempotency key: {Key}, TTL: {TTL}", idempotencyKey, ttl);
        }
    }
}

4. Entegrasyon ve Kullanım

Program.cs içinde gerekli servisleri kaydedin ve controller’da attribute’u kullanın:

builder.Services.AddMemoryCache();
builder.Services.AddScoped<IIdempotencyStore, MemoryCacheIdempotencyStore>();
builder.Services.AddScoped<IdempotencyFilter>();
builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();
[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
    // Default: TTL=300s, Header=Idempotency-Key
    [HttpPost]
    [Idempotent]
    public IActionResult CreatePayment([FromBody] PaymentDto dto)
    {
        _logger.LogInformation("Processing payment for amount: {Amount} {Currency}", dto.Amount, dto.Currency);

        var response = new PaymentResponse
        {
            Success = true,
            TransactionId = Guid.NewGuid().ToString(),
            Amount = dto.Amount,
            Currency = dto.Currency
        };

        return Ok(response);
    }

    [HttpPost("quick")]
    [Idempotent(TtlSeconds = 60, HeaderName = "X-Idemp-Key")]
    public IActionResult QuickPayment([FromBody] PaymentDto dto)
    {
        _logger.LogInformation("Processing quick payment for amount: {Amount} {Currency}", dto.Amount, dto.Currency);

        var response = new PaymentResponse
        {
            Success = true,
            TransactionId = Guid.NewGuid().ToString(),
            Amount = dto.Amount,
            Currency = dto.Currency,
            Status = "Quick-Processed"
        };

        return Ok(response);
    }
}

Sonuç

Bu attribute-tabanlı idempotency yaklaşımı ile:

  • Sadece ihtiyaç duyduğunuz endpoint’leri hedefleyebilir,
  • Dinamik parametreler ile TTL ve header adını hızlıca özelleştirebilir,
  • In-memory cache ile düşük ek yük sağlayarak sisteminizi daha güvenilir hale getirebilirsiniz.

.NET Core API’lerinizde idempotency kontrolünü birkaç satır attribute ile tamamen kontrolünüz altına alın!

Kaynakça

  1. Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures.
  2. HTTP/1.1: Semantics and Content (RFC 7231). IETF.
  3. Microsoft. (n.d.). Filters in ASP.NET Core. Retrieved from https://docs.microsoft.com/aspnet/core/mvc/controllers/filters
  4. Microsoft. (n.d.). IMemoryCache Interface. Retrieved from https://docs.microsoft.com/dotnet/api/microsoft.extensions.caching.memory.imemorycache
Github: .NET Core’da Idempotency (Attribute-Tabanlı) : Dinamik, Esnek ve In-Memory Çözüm Örneği

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

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