.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
- Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures.
- HTTP/1.1: Semantics and Content (RFC 7231). IETF.
- Microsoft. (n.d.). Filters in ASP.NET Core. Retrieved from https://docs.microsoft.com/aspnet/core/mvc/controllers/filters
- Microsoft. (n.d.). IMemoryCache Interface. Retrieved from https://docs.microsoft.com/dotnet/api/microsoft.extensions.caching.memory.imemorycache