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:
126
LittleShop.Client/Services/AuthenticationService.cs
Normal file
126
LittleShop.Client/Services/AuthenticationService.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net.Http.Json;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public class AuthenticationService : IAuthenticationService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<AuthenticationService> _logger;
|
||||
private string? _currentToken;
|
||||
private DateTime? _tokenExpiry;
|
||||
|
||||
public AuthenticationService(HttpClient httpClient, ILogger<AuthenticationService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<LoginResponse>> LoginAsync(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new LoginRequest { Username = username, Password = password };
|
||||
var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var loginResponse = await response.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
if (loginResponse != null)
|
||||
{
|
||||
SetToken(loginResponse.Token);
|
||||
return ApiResponse<LoginResponse>.Success(loginResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<LoginResponse>.Failure(
|
||||
error ?? "Login failed",
|
||||
response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Login failed");
|
||||
return ApiResponse<LoginResponse>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<LoginResponse>> RefreshTokenAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentToken))
|
||||
{
|
||||
return ApiResponse<LoginResponse>.Failure(
|
||||
"No token to refresh",
|
||||
System.Net.HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _currentToken);
|
||||
|
||||
var response = await _httpClient.PostAsync("api/auth/refresh", null);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var loginResponse = await response.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
if (loginResponse != null)
|
||||
{
|
||||
SetToken(loginResponse.Token);
|
||||
return ApiResponse<LoginResponse>.Success(loginResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return ApiResponse<LoginResponse>.Failure(
|
||||
"Token refresh failed",
|
||||
response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Token refresh failed");
|
||||
return ApiResponse<LoginResponse>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetToken(string token)
|
||||
{
|
||||
_currentToken = token;
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
// Parse token to get expiry
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jsonToken = handler.ReadJwtToken(token);
|
||||
_tokenExpiry = jsonToken.ValidTo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse token expiry");
|
||||
_tokenExpiry = DateTime.UtcNow.AddHours(1); // Default to 1 hour
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetToken() => _currentToken;
|
||||
|
||||
public bool IsAuthenticated()
|
||||
{
|
||||
return !string.IsNullOrEmpty(_currentToken) &&
|
||||
_tokenExpiry.HasValue &&
|
||||
_tokenExpiry.Value > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
_currentToken = null;
|
||||
_tokenExpiry = null;
|
||||
_httpClient.DefaultRequestHeaders.Authorization = null;
|
||||
}
|
||||
}
|
||||
138
LittleShop.Client/Services/CatalogService.cs
Normal file
138
LittleShop.Client/Services/CatalogService.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Net.Http.Json;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public class CatalogService : ICatalogService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<CatalogService> _logger;
|
||||
|
||||
public CatalogService(HttpClient httpClient, ILogger<CatalogService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<List<Category>>> GetCategoriesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/catalog/categories");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var categories = await response.Content.ReadFromJsonAsync<List<Category>>();
|
||||
return ApiResponse<List<Category>>.Success(categories ?? new List<Category>());
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<List<Category>>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get categories");
|
||||
return ApiResponse<List<Category>>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<Category>> GetCategoryByIdAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/catalog/categories/{id}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var category = await response.Content.ReadFromJsonAsync<Category>();
|
||||
if (category != null)
|
||||
return ApiResponse<Category>.Success(category);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<Category>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get category {CategoryId}", id);
|
||||
return ApiResponse<Category>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<PagedResult<Product>>> GetProductsAsync(
|
||||
int pageNumber = 1,
|
||||
int pageSize = 20,
|
||||
Guid? categoryId = null,
|
||||
string? searchTerm = null,
|
||||
decimal? minPrice = null,
|
||||
decimal? maxPrice = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new List<string>
|
||||
{
|
||||
$"pageNumber={pageNumber}",
|
||||
$"pageSize={pageSize}"
|
||||
};
|
||||
|
||||
if (categoryId.HasValue)
|
||||
queryParams.Add($"categoryId={categoryId.Value}");
|
||||
if (!string.IsNullOrEmpty(searchTerm))
|
||||
queryParams.Add($"search={Uri.EscapeDataString(searchTerm)}");
|
||||
if (minPrice.HasValue)
|
||||
queryParams.Add($"minPrice={minPrice.Value}");
|
||||
if (maxPrice.HasValue)
|
||||
queryParams.Add($"maxPrice={maxPrice.Value}");
|
||||
|
||||
var queryString = string.Join("&", queryParams);
|
||||
var response = await _httpClient.GetAsync($"api/catalog/products?{queryString}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<PagedResult<Product>>();
|
||||
if (result != null)
|
||||
return ApiResponse<PagedResult<Product>>.Success(result);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<PagedResult<Product>>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get products");
|
||||
return ApiResponse<PagedResult<Product>>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<Product>> GetProductByIdAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/catalog/products/{id}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var product = await response.Content.ReadFromJsonAsync<Product>();
|
||||
if (product != null)
|
||||
return ApiResponse<Product>.Success(product);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<Product>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get product {ProductId}", id);
|
||||
return ApiResponse<Product>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
LittleShop.Client/Services/IAuthenticationService.cs
Normal file
13
LittleShop.Client/Services/IAuthenticationService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using LittleShop.Client.Models;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
Task<ApiResponse<LoginResponse>> LoginAsync(string username, string password);
|
||||
Task<ApiResponse<LoginResponse>> RefreshTokenAsync();
|
||||
void SetToken(string token);
|
||||
string? GetToken();
|
||||
bool IsAuthenticated();
|
||||
void Logout();
|
||||
}
|
||||
17
LittleShop.Client/Services/ICatalogService.cs
Normal file
17
LittleShop.Client/Services/ICatalogService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using LittleShop.Client.Models;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public interface ICatalogService
|
||||
{
|
||||
Task<ApiResponse<List<Category>>> GetCategoriesAsync();
|
||||
Task<ApiResponse<Category>> GetCategoryByIdAsync(Guid id);
|
||||
Task<ApiResponse<PagedResult<Product>>> GetProductsAsync(
|
||||
int pageNumber = 1,
|
||||
int pageSize = 20,
|
||||
Guid? categoryId = null,
|
||||
string? searchTerm = null,
|
||||
decimal? minPrice = null,
|
||||
decimal? maxPrice = null);
|
||||
Task<ApiResponse<Product>> GetProductByIdAsync(Guid id);
|
||||
}
|
||||
12
LittleShop.Client/Services/IOrderService.cs
Normal file
12
LittleShop.Client/Services/IOrderService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using LittleShop.Client.Models;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public interface IOrderService
|
||||
{
|
||||
Task<ApiResponse<Order>> CreateOrderAsync(CreateOrderRequest request);
|
||||
Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference);
|
||||
Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id);
|
||||
Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, string currency);
|
||||
Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId);
|
||||
}
|
||||
143
LittleShop.Client/Services/OrderService.cs
Normal file
143
LittleShop.Client/Services/OrderService.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.Net.Http.Json;
|
||||
using LittleShop.Client.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LittleShop.Client.Services;
|
||||
|
||||
public class OrderService : IOrderService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<OrderService> _logger;
|
||||
|
||||
public OrderService(HttpClient httpClient, ILogger<OrderService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<Order>> CreateOrderAsync(CreateOrderRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("api/orders", request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var order = await response.Content.ReadFromJsonAsync<Order>();
|
||||
if (order != null)
|
||||
return ApiResponse<Order>.Success(order, response.StatusCode);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<Order>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create order");
|
||||
return ApiResponse<Order>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(
|
||||
$"api/orders/by-identity/{Uri.EscapeDataString(identityReference)}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var orders = await response.Content.ReadFromJsonAsync<List<Order>>();
|
||||
return ApiResponse<List<Order>>.Success(orders ?? new List<Order>());
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<List<Order>>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get orders for identity {Identity}", identityReference);
|
||||
return ApiResponse<List<Order>>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/orders/{id}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var order = await response.Content.ReadFromJsonAsync<Order>();
|
||||
if (order != null)
|
||||
return ApiResponse<Order>.Success(order);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<Order>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get order {OrderId}", id);
|
||||
return ApiResponse<Order>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, string currency)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new CreatePaymentRequest { Currency = currency };
|
||||
var response = await _httpClient.PostAsJsonAsync(
|
||||
$"api/orders/{orderId}/payments", request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payment = await response.Content.ReadFromJsonAsync<CryptoPayment>();
|
||||
if (payment != null)
|
||||
return ApiResponse<CryptoPayment>.Success(payment, response.StatusCode);
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<CryptoPayment>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create payment for order {OrderId}", orderId);
|
||||
return ApiResponse<CryptoPayment>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"api/orders/{orderId}/payments");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payments = await response.Content.ReadFromJsonAsync<List<CryptoPayment>>();
|
||||
return ApiResponse<List<CryptoPayment>>.Success(payments ?? new List<CryptoPayment>());
|
||||
}
|
||||
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return ApiResponse<List<CryptoPayment>>.Failure(error, response.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get payments for order {OrderId}", orderId);
|
||||
return ApiResponse<List<CryptoPayment>>.Failure(
|
||||
ex.Message,
|
||||
System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user