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:
sysadmin
2025-08-20 18:15:35 +01:00
parent a281bb2896
commit 1f7c0af497
28 changed files with 1810 additions and 33 deletions

View File

@@ -0,0 +1,6 @@
namespace LittleShop.Client;
public class Class1
{
}

View File

@@ -0,0 +1,10 @@
namespace LittleShop.Client.Configuration;
public class LittleShopClientOptions
{
public string BaseUrl { get; set; } = "https://localhost:5001";
public int TimeoutSeconds { get; set; } = 30;
public int MaxRetryAttempts { get; set; } = 3;
public bool EnableLogging { get; set; } = true;
public string? ApiKey { get; set; }
}

View File

@@ -0,0 +1,91 @@
using LittleShop.Client.Configuration;
using LittleShop.Client.Http;
using LittleShop.Client.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace LittleShop.Client.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddLittleShopClient(
this IServiceCollection services,
Action<LittleShopClientOptions>? configureOptions = null)
{
// Configure options
if (configureOptions != null)
{
services.Configure(configureOptions);
}
else
{
services.Configure<LittleShopClientOptions>(options => { });
}
// Register HTTP handlers
services.AddTransient<RetryPolicyHandler>();
services.AddTransient<ErrorHandlingMiddleware>();
// Register main HTTP client
services.AddHttpClient<IAuthenticationService, AuthenticationService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<ErrorHandlingMiddleware>()
.AddHttpMessageHandler(serviceProvider =>
{
var logger = serviceProvider.GetRequiredService<ILogger<RetryPolicyHandler>>();
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
return new RetryPolicyHandler(logger, options.MaxRetryAttempts);
});
services.AddHttpClient<ICatalogService, CatalogService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<ErrorHandlingMiddleware>()
.AddHttpMessageHandler(serviceProvider =>
{
var logger = serviceProvider.GetRequiredService<ILogger<RetryPolicyHandler>>();
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
return new RetryPolicyHandler(logger, options.MaxRetryAttempts);
});
services.AddHttpClient<IOrderService, OrderService>((serviceProvider, client) =>
{
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddHttpMessageHandler<ErrorHandlingMiddleware>()
.AddHttpMessageHandler(serviceProvider =>
{
var logger = serviceProvider.GetRequiredService<ILogger<RetryPolicyHandler>>();
var options = serviceProvider.GetRequiredService<IOptions<LittleShopClientOptions>>().Value;
return new RetryPolicyHandler(logger, options.MaxRetryAttempts);
});
// Register the main client
services.AddScoped<ILittleShopClient, LittleShopClient>();
return services;
}
public static IServiceCollection AddLittleShopClient(
this IServiceCollection services,
string baseUrl)
{
return services.AddLittleShopClient(options =>
{
options.BaseUrl = baseUrl;
});
}
}

View 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; }
}
}

View 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));
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
using LittleShop.Client.Configuration;
using LittleShop.Client.Services;
using Microsoft.Extensions.Options;
namespace LittleShop.Client;
public interface ILittleShopClient
{
IAuthenticationService Authentication { get; }
ICatalogService Catalog { get; }
IOrderService Orders { get; }
}
public class LittleShopClient : ILittleShopClient
{
public IAuthenticationService Authentication { get; }
public ICatalogService Catalog { get; }
public IOrderService Orders { get; }
public LittleShopClient(
IAuthenticationService authenticationService,
ICatalogService catalogService,
IOrderService orderService)
{
Authentication = authenticationService;
Catalog = catalogService;
Orders = orderService;
}
}

View File

@@ -0,0 +1,40 @@
using System.Net;
namespace LittleShop.Client.Models;
public class ApiResponse<T>
{
public bool IsSuccess { get; set; }
public T? Data { get; set; }
public string? ErrorMessage { get; set; }
public HttpStatusCode StatusCode { get; set; }
public static ApiResponse<T> Success(T data, HttpStatusCode statusCode = HttpStatusCode.OK)
{
return new ApiResponse<T>
{
IsSuccess = true,
Data = data,
StatusCode = statusCode
};
}
public static ApiResponse<T> Failure(string errorMessage, HttpStatusCode statusCode)
{
return new ApiResponse<T>
{
IsSuccess = false,
ErrorMessage = errorMessage,
StatusCode = statusCode
};
}
}
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
}

View File

@@ -0,0 +1,11 @@
namespace LittleShop.Client.Models;
public class Category
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace LittleShop.Client.Models;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
}

View File

@@ -0,0 +1,51 @@
namespace LittleShop.Client.Models;
public class Order
{
public Guid Id { get; set; }
public string IdentityReference { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public string Currency { get; set; } = string.Empty;
public string ShippingName { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public string ShippingCity { get; set; } = string.Empty;
public string ShippingPostCode { get; set; } = string.Empty;
public string ShippingCountry { get; set; } = string.Empty;
public string? Notes { get; set; }
public string? TrackingNumber { get; set; }
public List<OrderItem> Items { get; set; } = new();
public List<CryptoPayment> Payments { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? PaidAt { get; set; }
public DateTime? ShippedAt { get; set; }
}
public class OrderItem
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public string? ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
}
public class CreateOrderRequest
{
public string IdentityReference { get; set; } = string.Empty;
public string ShippingName { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public string ShippingCity { get; set; } = string.Empty;
public string ShippingPostCode { get; set; } = string.Empty;
public string ShippingCountry { get; set; } = "United Kingdom";
public string? Notes { get; set; }
public List<CreateOrderItem> Items { get; set; } = new();
}
public class CreateOrderItem
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
}

View File

@@ -0,0 +1,23 @@
namespace LittleShop.Client.Models;
public class CryptoPayment
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public string Currency { get; set; } = string.Empty;
public string WalletAddress { get; set; } = string.Empty;
public decimal RequiredAmount { get; set; }
public decimal? PaidAmount { get; set; }
public string Status { get; set; } = string.Empty;
public string? TransactionHash { get; set; }
public string? BTCPayInvoiceId { get; set; }
public string? BTCPayCheckoutUrl { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? PaidAt { get; set; }
public DateTime ExpiresAt { get; set; }
}
public class CreatePaymentRequest
{
public string Currency { get; set; } = "BTC";
}

View File

@@ -0,0 +1,25 @@
namespace LittleShop.Client.Models;
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public decimal Weight { get; set; }
public string WeightUnit { get; set; } = string.Empty;
public Guid CategoryId { get; set; }
public string? CategoryName { get; set; }
public bool IsActive { get; set; }
public List<ProductPhoto> Photos { get; set; } = new();
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class ProductPhoto
{
public Guid Id { get; set; }
public string Url { get; set; } = string.Empty;
public string? AltText { get; set; }
public int SortOrder { get; set; }
}

373
LittleShop.Client/README.md Normal file
View File

@@ -0,0 +1,373 @@
# LittleShop Client SDK
A .NET client library for interacting with the LittleShop e-commerce API. This SDK provides a strongly-typed, easy-to-use interface for all LittleShop API endpoints with built-in authentication, retry policies, and error handling.
## Installation
Add the LittleShop.Client library to your project:
```bash
dotnet add reference ../LittleShop.Client/LittleShop.Client.csproj
```
Or via NuGet (when published):
```bash
dotnet add package LittleShop.Client
```
## Quick Start
### Basic Setup with Dependency Injection
```csharp
using LittleShop.Client.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
// Add LittleShop client with configuration
services.AddLittleShopClient(options =>
{
options.BaseUrl = "https://localhost:5001";
options.TimeoutSeconds = 30;
options.MaxRetryAttempts = 3;
options.EnableLogging = true;
});
})
.Build();
// Get the client from DI
var client = host.Services.GetRequiredService<ILittleShopClient>();
```
### Simple Console Application Example
```csharp
using LittleShop.Client;
using LittleShop.Client.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// Setup DI container
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
services.AddLittleShopClient("https://localhost:5001");
var serviceProvider = services.BuildServiceProvider();
var client = serviceProvider.GetRequiredService<ILittleShopClient>();
// Authenticate
var loginResult = await client.Authentication.LoginAsync("admin", "admin");
if (loginResult.IsSuccess)
{
Console.WriteLine($"Logged in as: {loginResult.Data.Username}");
Console.WriteLine($"Token expires: {loginResult.Data.ExpiresAt}");
}
// Get categories
var categoriesResult = await client.Catalog.GetCategoriesAsync();
if (categoriesResult.IsSuccess)
{
foreach (var category in categoriesResult.Data)
{
Console.WriteLine($"Category: {category.Name} - {category.Description}");
}
}
// Get products with filtering
var productsResult = await client.Catalog.GetProductsAsync(
pageNumber: 1,
pageSize: 10,
searchTerm: "wireless",
minPrice: 10,
maxPrice: 100
);
if (productsResult.IsSuccess)
{
Console.WriteLine($"Found {productsResult.Data.TotalCount} products");
foreach (var product in productsResult.Data.Items)
{
Console.WriteLine($"Product: {product.Name} - £{product.Price}");
}
}
```
## Authentication
The SDK handles JWT authentication automatically. Once you log in, the token is stored and automatically included in subsequent requests.
```csharp
// Login
var loginResult = await client.Authentication.LoginAsync("username", "password");
if (!loginResult.IsSuccess)
{
Console.WriteLine($"Login failed: {loginResult.ErrorMessage}");
return;
}
// Check if authenticated
if (client.Authentication.IsAuthenticated())
{
Console.WriteLine("User is authenticated");
}
// Get current token (if needed for external use)
var token = client.Authentication.GetToken();
// Logout
client.Authentication.Logout();
```
## Catalog Operations
### Get All Categories
```csharp
var result = await client.Catalog.GetCategoriesAsync();
if (result.IsSuccess)
{
foreach (var category in result.Data)
{
Console.WriteLine($"{category.Name}: {category.Description}");
}
}
```
### Get Products with Pagination and Filtering
```csharp
var result = await client.Catalog.GetProductsAsync(
pageNumber: 1,
pageSize: 20,
categoryId: categoryGuid, // Optional: Filter by category
searchTerm: "headphones", // Optional: Search term
minPrice: 50, // Optional: Minimum price
maxPrice: 200 // Optional: Maximum price
);
if (result.IsSuccess)
{
Console.WriteLine($"Page {result.Data.PageNumber} of {result.Data.TotalPages}");
Console.WriteLine($"Total products: {result.Data.TotalCount}");
foreach (var product in result.Data.Items)
{
Console.WriteLine($"{product.Name} - £{product.Price}");
if (product.Photos.Any())
{
Console.WriteLine($" Photo: {product.Photos.First().Url}");
}
}
}
```
### Get Single Product
```csharp
var result = await client.Catalog.GetProductByIdAsync(productId);
if (result.IsSuccess)
{
var product = result.Data;
Console.WriteLine($"Name: {product.Name}");
Console.WriteLine($"Price: £{product.Price}");
Console.WriteLine($"Weight: {product.Weight} {product.WeightUnit}");
}
```
## Order Operations
### Create Order
```csharp
var orderRequest = new CreateOrderRequest
{
IdentityReference = "CUST123",
ShippingName = "John Smith",
ShippingAddress = "123 Main Street",
ShippingCity = "London",
ShippingPostCode = "SW1A 1AA",
ShippingCountry = "United Kingdom",
Notes = "Please deliver to reception",
Items = new List<CreateOrderItem>
{
new CreateOrderItem
{
ProductId = productId1,
Quantity = 2
},
new CreateOrderItem
{
ProductId = productId2,
Quantity = 1
}
}
};
var result = await client.Orders.CreateOrderAsync(orderRequest);
if (result.IsSuccess)
{
Console.WriteLine($"Order created: {result.Data.Id}");
Console.WriteLine($"Total: £{result.Data.TotalAmount}");
}
```
### Get Orders by Customer Identity
```csharp
var result = await client.Orders.GetOrdersByIdentityAsync("CUST123");
if (result.IsSuccess)
{
foreach (var order in result.Data)
{
Console.WriteLine($"Order {order.Id}: {order.Status} - £{order.TotalAmount}");
Console.WriteLine($" Created: {order.CreatedAt}");
if (!string.IsNullOrEmpty(order.TrackingNumber))
{
Console.WriteLine($" Tracking: {order.TrackingNumber}");
}
}
}
```
### Create Cryptocurrency Payment
```csharp
var result = await client.Orders.CreatePaymentAsync(orderId, "BTC");
if (result.IsSuccess)
{
var payment = result.Data;
Console.WriteLine($"Payment ID: {payment.Id}");
Console.WriteLine($"Send {payment.RequiredAmount} {payment.Currency} to:");
Console.WriteLine($"Address: {payment.WalletAddress}");
Console.WriteLine($"Expires: {payment.ExpiresAt}");
if (!string.IsNullOrEmpty(payment.BTCPayCheckoutUrl))
{
Console.WriteLine($"Or pay via: {payment.BTCPayCheckoutUrl}");
}
}
```
## Error Handling
All API methods return an `ApiResponse<T>` wrapper that includes success status, data, and error information:
```csharp
var result = await client.Catalog.GetProductByIdAsync(productId);
if (result.IsSuccess)
{
// Success - access data
var product = result.Data;
Console.WriteLine($"Product: {product.Name}");
}
else
{
// Error - handle appropriately
Console.WriteLine($"Error: {result.ErrorMessage}");
Console.WriteLine($"Status Code: {result.StatusCode}");
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
Console.WriteLine("Product not found");
break;
case HttpStatusCode.Unauthorized:
Console.WriteLine("Please login first");
break;
case HttpStatusCode.BadRequest:
Console.WriteLine("Invalid request");
break;
default:
Console.WriteLine("An error occurred");
break;
}
}
```
## Advanced Configuration
### Custom HTTP Client Configuration
The SDK uses Polly for retry policies and includes automatic retry for transient failures:
```csharp
services.AddLittleShopClient(options =>
{
options.BaseUrl = "https://api.littleshop.com";
options.TimeoutSeconds = 60; // Request timeout
options.MaxRetryAttempts = 5; // Number of retries for transient failures
options.EnableLogging = true; // Enable detailed logging
});
```
### Using in ASP.NET Core Web Application
```csharp
// In Program.cs or Startup.cs
builder.Services.AddLittleShopClient(options =>
{
options.BaseUrl = builder.Configuration["LittleShop:ApiUrl"];
options.TimeoutSeconds = 30;
});
// In a controller or service
public class ProductController : Controller
{
private readonly ILittleShopClient _client;
public ProductController(ILittleShopClient client)
{
_client = client;
}
public async Task<IActionResult> Index()
{
var result = await _client.Catalog.GetProductsAsync();
if (result.IsSuccess)
{
return View(result.Data.Items);
}
return View("Error", result.ErrorMessage);
}
}
```
## Features
-**Strongly Typed Models** - All API responses are mapped to C# classes
-**Automatic Authentication** - JWT tokens are managed automatically
-**Retry Policies** - Automatic retry for transient failures
-**Error Handling** - Consistent error responses with detailed information
-**Logging Support** - Built-in logging for debugging
-**Dependency Injection** - Full DI support for ASP.NET Core applications
-**Async/Await** - All methods are async for better performance
-**Pagination Support** - Built-in pagination for product listings
-**Filtering & Search** - Advanced filtering options for products
## API Coverage
- **Authentication**
- Login
- Token refresh
- Logout
- **Catalog**
- Get all categories
- Get category by ID
- Get products (with pagination and filtering)
- Get product by ID
- **Orders**
- Create order
- Get orders by customer identity
- Get order by ID
- Create cryptocurrency payment
- Get order payments
## Requirements
- .NET 9.0 or later
- LittleShop API server running and accessible
## License
This SDK is part of the LittleShop e-commerce platform.

View 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;
}
}

View 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);
}
}
}

View 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();
}

View 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);
}

View 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);
}

View 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);
}
}
}