Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
feat: handle idempotency API
Browse files Browse the repository at this point in the history
  • Loading branch information
foxminchan committed May 12, 2024
1 parent 2e788a3 commit ff9fb07
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 13 deletions.
34 changes: 34 additions & 0 deletions src/RookieShop.ApiService/Filters/IdempotencyFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using FluentValidation;
using RookieShop.Infrastructure.Cache.Redis;

namespace RookieShop.ApiService.Filters;

public class IdempotencyFilter(IRedisService redisService) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var request = context.HttpContext.Request;
var requestMethod = request.Method;
var requestPath = request.Path;
var requestId = request.Headers["X-Idempotency-Key"].FirstOrDefault();

if (requestMethod is not "POST" and not "PATCH")
return await next(context);

if (string.IsNullOrEmpty(requestId))
{
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
throw new ValidationException("X-Idempotency-Key header is required for POST and PATCH requests.");
}

var cacheKey = $"{requestMethod}:{requestPath}:{requestId}";

var cacheValue = await redisService.GetOrSet(cacheKey, () => request.GetType().Name, TimeSpan.FromMinutes(1));

if (string.IsNullOrEmpty(cacheValue))
return await next(context);

context.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
return TypedResults.Conflict();
}
}
48 changes: 35 additions & 13 deletions src/RookieShop.ApiService/Middlewares/ExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,54 @@ public async ValueTask<bool> TryHandleAsync(
return true;
}

private static async Task HandleValidationException(
private static async ValueTask<bool> HandleValidationException(
HttpContext httpContext,
ValidationException validationException,
CancellationToken cancellationToken)
{
var validationErrorModel = Result.Invalid(validationException
.Errors
.Select(e => new ValidationError(
e.PropertyName,
e.ErrorMessage,
StatusCodes.Status400BadRequest.ToString(),
ValidationSeverity.Info
)).ToList());
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(TypedResults.BadRequest(validationErrorModel.ValidationErrors),
cancellationToken);

if (validationException.Errors.Any())
{
var validationErrorModel = Result.Invalid(validationException
.Errors
.Select(e => new ValidationError(
e.PropertyName,
e.ErrorMessage,
StatusCodes.Status400BadRequest.ToString(),
ValidationSeverity.Info
)).ToList());

httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;

await httpContext.Response.WriteAsJsonAsync(TypedResults.BadRequest(validationErrorModel.ValidationErrors),
cancellationToken);
}
else
{
await httpContext.Response.WriteAsJsonAsync(TypedResults.BadRequest(validationException.Message),
cancellationToken);
}

return true;
}

private static async Task HandleNotFoundException(
private static async ValueTask<bool> HandleNotFoundException(
HttpContext httpContext,
Exception notFoundException,
CancellationToken cancellationToken)
{
var notFoundErrorModel = Result.NotFound(notFoundException.Message);

httpContext.Response.StatusCode = StatusCodes.Status404NotFound;

await httpContext.Response.WriteAsJsonAsync(TypedResults.NotFound(notFoundErrorModel.Errors[0]),
cancellationToken);

return true;
}

private static async Task HandleDefaultException(
private static async ValueTask<bool> HandleDefaultException(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
Expand All @@ -76,7 +94,11 @@ private static async Task HandleDefaultException(
Detail = exception.Message,
Instance = $"{httpContext.Request.Method}{httpContext.Request.Path}"
};

httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;

await httpContext.Response.WriteAsJsonAsync(details, cancellationToken);

return true;
}
}
3 changes: 3 additions & 0 deletions src/RookieShop.ApiService/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.Json;
using Ardalis.ListStartupServices;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using RookieShop.ApiService.Filters;
using RookieShop.ApiService.Middlewares;
using RookieShop.Application;
using RookieShop.Infrastructure;
Expand All @@ -27,6 +28,8 @@

builder.Services.AddAntiforgery();

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddExceptionHandler<ExceptionHandler>();

builder.Services.AddProblemDetails();
Expand Down

0 comments on commit ff9fb07

Please sign in to comment.