Add LittleShop.Client SDK library with complete API wrapper
Features: - Complete .NET client SDK for LittleShop API - JWT authentication with automatic token management - Catalog service for products and categories - Order service with payment creation - Retry policies using Polly for resilience - Error handling middleware - Dependency injection support - Comprehensive documentation and examples SDK Components: - Authentication service with token refresh - Strongly-typed models for all API responses - HTTP handlers for retry and error handling - Extension methods for easy DI registration - Example console application demonstrating usage Test Updates: - Fixed test compilation errors - Updated test data builders for new models - Corrected service constructor dependencies - Fixed enum value changes (PaymentStatus, OrderStatus) Documentation: - Complete project README with features and usage - Client SDK README with detailed examples - API endpoint documentation - Security considerations - Deployment guidelines Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
99
LittleShop.Client/Http/ErrorHandlingMiddleware.cs
Normal file
99
LittleShop.Client/Http/ErrorHandlingMiddleware.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LittleShop.Client.Http;
|
||||
|
||||
public class ErrorHandlingMiddleware : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger<ErrorHandlingMiddleware> _logger;
|
||||
|
||||
public ErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
// Log errors but don't throw - let the service layer handle the response
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
_logger.LogWarning(
|
||||
"HTTP {StatusCode} for {Method} {Uri}: {Content}",
|
||||
(int)response.StatusCode,
|
||||
request.Method,
|
||||
request.RequestUri,
|
||||
content);
|
||||
|
||||
// Try to parse error response
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(content,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (errorResponse != null && !string.IsNullOrEmpty(errorResponse.Message))
|
||||
{
|
||||
// Replace content with structured error message
|
||||
response.Content = new StringContent(errorResponse.Message);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't parse the error, leave original content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Request timeout for {Method} {Uri}",
|
||||
request.Method, request.RequestUri);
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.RequestTimeout)
|
||||
{
|
||||
Content = new StringContent("Request timed out"),
|
||||
RequestMessage = request
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Network error for {Method} {Uri}",
|
||||
request.Method, request.RequestUri);
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
Content = new StringContent($"Network error: {ex.Message}"),
|
||||
RequestMessage = request
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error for {Method} {Uri}",
|
||||
request.Method, request.RequestUri);
|
||||
|
||||
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||
{
|
||||
Content = new StringContent($"Unexpected error: {ex.Message}"),
|
||||
RequestMessage = request
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class ErrorResponse
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public Dictionary<string, string[]>? Errors { get; set; }
|
||||
}
|
||||
}
|
||||
39
LittleShop.Client/Http/RetryPolicyHandler.cs
Normal file
39
LittleShop.Client/Http/RetryPolicyHandler.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
|
||||
namespace LittleShop.Client.Http;
|
||||
|
||||
public class RetryPolicyHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger<RetryPolicyHandler> _logger;
|
||||
private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
|
||||
|
||||
public RetryPolicyHandler(ILogger<RetryPolicyHandler> logger, int maxRetryAttempts = 3)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
_retryPolicy = HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.OrResult(msg => !msg.IsSuccessStatusCode && msg.StatusCode != HttpStatusCode.Unauthorized)
|
||||
.WaitAndRetryAsync(
|
||||
maxRetryAttempts,
|
||||
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
|
||||
onRetry: (outcome, timespan, retryCount, context) =>
|
||||
{
|
||||
var reason = outcome.Result?.StatusCode.ToString() ?? outcome.Exception?.Message;
|
||||
_logger.LogWarning(
|
||||
"Retry {RetryCount} after {TimeSpan}ms due to: {Reason}",
|
||||
retryCount, timespan.TotalMilliseconds, reason);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await _retryPolicy.ExecuteAsync(async () =>
|
||||
await base.SendAsync(request, cancellationToken));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user