Init
This commit is contained in:
32
Prefab.Shared/Catalog/Categories/CategoryClientHttp.cs
Normal file
32
Prefab.Shared/Catalog/Categories/CategoryClientHttp.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Prefab.Shared.Catalog.Categories;
|
||||
|
||||
public sealed class CategoryClientHttp(HttpClient httpClient) : ICategoryClient
|
||||
{
|
||||
public async Task<GetCategories.Response> GetCategories(CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await httpClient.GetAsync("api/catalog/categories", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GetCategories.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Received an empty response when requesting categories.");
|
||||
}
|
||||
|
||||
public async Task<CreateCategory.Response> CreateCategory(CreateCategory.Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await httpClient.PostAsJsonAsync("api/catalog/categories", request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<CreateCategory.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Received an empty response when creating a category.");
|
||||
}
|
||||
}
|
||||
11
Prefab.Shared/Catalog/Categories/CreateCategory.cs
Normal file
11
Prefab.Shared/Catalog/Categories/CreateCategory.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Prefab.Shared.Catalog.Categories;
|
||||
|
||||
public class CreateCategory
|
||||
{
|
||||
public record Request(
|
||||
string Name,
|
||||
string Description);
|
||||
|
||||
public record Response(
|
||||
Guid Id);
|
||||
}
|
||||
8
Prefab.Shared/Catalog/Categories/GetCategories.cs
Normal file
8
Prefab.Shared/Catalog/Categories/GetCategories.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Prefab.Shared.Catalog.Categories;
|
||||
|
||||
public abstract class GetCategories
|
||||
{
|
||||
public record Response(IReadOnlyList<ListItem> Categories);
|
||||
|
||||
public record ListItem(Guid Id, string Name, string ParentName);
|
||||
}
|
||||
7
Prefab.Shared/Catalog/Categories/ICategoryClient.cs
Normal file
7
Prefab.Shared/Catalog/Categories/ICategoryClient.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Prefab.Shared.Catalog.Categories;
|
||||
|
||||
public interface ICategoryClient
|
||||
{
|
||||
Task<GetCategories.Response> GetCategories(CancellationToken cancellationToken);
|
||||
Task<CreateCategory.Response> CreateCategory(CreateCategory.Request request, CancellationToken cancellationToken);
|
||||
}
|
||||
12
Prefab.Shared/Catalog/IModuleClient.cs
Normal file
12
Prefab.Shared/Catalog/IModuleClient.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Prefab.Shared.Catalog.Categories;
|
||||
|
||||
namespace Prefab.Shared.Catalog;
|
||||
|
||||
public interface IModuleClient
|
||||
{
|
||||
public ICategoryClient Category { get; }
|
||||
|
||||
public Products.IProductClient Product { get; }
|
||||
|
||||
public Products.IPriceQuoteClient PriceQuote { get; }
|
||||
}
|
||||
23
Prefab.Shared/Catalog/ModuleClientHttp.cs
Normal file
23
Prefab.Shared/Catalog/ModuleClientHttp.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Prefab.Shared.Catalog.Categories;
|
||||
using Prefab.Shared.Catalog.Products;
|
||||
|
||||
namespace Prefab.Shared.Catalog;
|
||||
|
||||
/// <summary>
|
||||
/// Provides HTTP-based access to module-related operations, exposing product and category management functionality
|
||||
/// through their respective clients.
|
||||
/// </summary>
|
||||
/// <param name="categoryClient">The category client used to perform category-related operations. Cannot be null.</param>
|
||||
/// <param name="productClient">The product client used to retrieve product configuration. Cannot be null.</param>
|
||||
/// <param name="priceQuoteClient">The client used to request price quotations. Cannot be null.</param>
|
||||
public sealed class ModuleClientHttp(
|
||||
ICategoryClient categoryClient,
|
||||
IProductClient productClient,
|
||||
IPriceQuoteClient priceQuoteClient) : IModuleClient
|
||||
{
|
||||
public ICategoryClient Category { get; } = categoryClient;
|
||||
|
||||
public IProductClient Product { get; } = productClient;
|
||||
|
||||
public IPriceQuoteClient PriceQuote { get; } = priceQuoteClient;
|
||||
}
|
||||
8
Prefab.Shared/Catalog/Products/GetCategory.cs
Normal file
8
Prefab.Shared/Catalog/Products/GetCategory.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public abstract class GetCategory
|
||||
{
|
||||
public sealed record Request(string Slug);
|
||||
|
||||
public sealed record Response(GetCategoryModels.CategoryDetailDto Category);
|
||||
}
|
||||
50
Prefab.Shared/Catalog/Products/GetCategoryModels.cs
Normal file
50
Prefab.Shared/Catalog/Products/GetCategoryModels.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public abstract class GetCategoryModels
|
||||
{
|
||||
public sealed record Request(
|
||||
string Slug,
|
||||
int? Page,
|
||||
int? PageSize,
|
||||
string? Sort,
|
||||
string? Direction);
|
||||
|
||||
public sealed record Response(CategoryProductsResponse Result);
|
||||
|
||||
public sealed record CategoryProductsResponse(
|
||||
CategoryDetailDto Category,
|
||||
IReadOnlyList<ProductCardDto> Products,
|
||||
int Total,
|
||||
int Page,
|
||||
int PageSize);
|
||||
|
||||
public sealed record CategoryDetailDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? Description,
|
||||
string? HeroImageUrl,
|
||||
string? Icon,
|
||||
bool IsLeaf,
|
||||
IReadOnlyList<CategoryNodeDto> Children);
|
||||
|
||||
public sealed record CategoryNodeDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? Description,
|
||||
string? HeroImageUrl,
|
||||
string? Icon,
|
||||
int DisplayOrder,
|
||||
bool IsFeatured,
|
||||
bool IsLeaf,
|
||||
IReadOnlyList<CategoryNodeDto> Children);
|
||||
|
||||
public sealed record ProductCardDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Slug,
|
||||
decimal? FromPrice,
|
||||
string? PrimaryImageUrl,
|
||||
DateTimeOffset LastModifiedOn);
|
||||
}
|
||||
8
Prefab.Shared/Catalog/Products/GetCategoryTree.cs
Normal file
8
Prefab.Shared/Catalog/Products/GetCategoryTree.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public abstract class GetCategoryTree
|
||||
{
|
||||
public sealed record Request(int? Depth);
|
||||
|
||||
public sealed record Response(IReadOnlyList<GetCategoryModels.CategoryNodeDto> Categories);
|
||||
}
|
||||
77
Prefab.Shared/Catalog/Products/GetProductDetail.cs
Normal file
77
Prefab.Shared/Catalog/Products/GetProductDetail.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public abstract class GetProductDetail
|
||||
{
|
||||
public enum OptionDataType
|
||||
{
|
||||
Choice = 0,
|
||||
Number = 1
|
||||
}
|
||||
|
||||
public enum PriceDeltaKind
|
||||
{
|
||||
Absolute = 0,
|
||||
Percent = 1
|
||||
}
|
||||
|
||||
public sealed record Response(ProductDetailDto Product);
|
||||
|
||||
public sealed record ProductDetailDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? Description,
|
||||
decimal? Price,
|
||||
IReadOnlyList<OptionDefinitionDto> Options,
|
||||
IReadOnlyList<SpecDto> Specs,
|
||||
IReadOnlyDictionary<string, string>? GenericAttributes);
|
||||
|
||||
public sealed record OptionDefinitionDto(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string Name,
|
||||
int DataType,
|
||||
bool IsVariantAxis,
|
||||
string? Unit,
|
||||
decimal? Min,
|
||||
decimal? Max,
|
||||
decimal? Step,
|
||||
decimal? PricePerUnit,
|
||||
IReadOnlyList<OptionTierDto> Tiers,
|
||||
IReadOnlyList<OptionValueDto> Values,
|
||||
IReadOnlyList<RuleSetDto>? Rules);
|
||||
|
||||
public sealed record OptionValueDto(
|
||||
Guid Id,
|
||||
string Code,
|
||||
string Label,
|
||||
decimal? PriceDelta,
|
||||
int PriceDeltaKind,
|
||||
IReadOnlyList<RuleSetDto>? Rules);
|
||||
|
||||
public sealed record OptionTierDto(
|
||||
decimal FromInclusive,
|
||||
decimal? ToInclusive,
|
||||
decimal UnitRate,
|
||||
decimal? FlatDelta);
|
||||
|
||||
public sealed record RuleSetDto(
|
||||
string Effect,
|
||||
string Mode,
|
||||
IReadOnlyList<ConditionDto> Conditions);
|
||||
|
||||
public sealed record ConditionDto(
|
||||
string LeftOptionCode,
|
||||
string Operator,
|
||||
string? RightValueCode,
|
||||
string? RightList,
|
||||
decimal? RightNumber,
|
||||
decimal? RightMin,
|
||||
decimal? RightMax);
|
||||
|
||||
public sealed record SpecDto(
|
||||
string Name,
|
||||
string? Value,
|
||||
decimal? NumericValue,
|
||||
string? UnitCode);
|
||||
}
|
||||
19
Prefab.Shared/Catalog/Products/IProductClient.cs
Normal file
19
Prefab.Shared/Catalog/Products/IProductClient.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public interface IProductClient
|
||||
{
|
||||
Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken);
|
||||
|
||||
Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken);
|
||||
|
||||
Task<GetCategoryModels.Response> GetCategoryModels(string slug, int? page, int? pageSize, string? sort, string? direction, CancellationToken cancellationToken);
|
||||
|
||||
Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken);
|
||||
|
||||
Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IPriceQuoteClient
|
||||
{
|
||||
Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken);
|
||||
}
|
||||
117
Prefab.Shared/Catalog/Products/ProductClientHttp.cs
Normal file
117
Prefab.Shared/Catalog/Products/ProductClientHttp.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public sealed class ProductClientHttp(HttpClient httpClient) : IProductClient, IPriceQuoteClient
|
||||
{
|
||||
public async Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken)
|
||||
{
|
||||
var trimmedDepth = depth is > 0 ? depth : null;
|
||||
var path = "api/catalog/categories/tree";
|
||||
if (trimmedDepth.HasValue)
|
||||
{
|
||||
path = $"{path}?depth={trimmedDepth.Value}";
|
||||
}
|
||||
|
||||
using var response = await httpClient.GetAsync(path, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GetCategoryTree.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Category tree response payload was empty.");
|
||||
}
|
||||
|
||||
public async Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
|
||||
|
||||
using var response = await httpClient.GetAsync($"api/catalog/categories/{slug.Trim()}", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GetCategory.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Category detail response payload was empty.");
|
||||
}
|
||||
|
||||
public async Task<GetCategoryModels.Response> GetCategoryModels(string slug, int? page, int? pageSize, string? sort, string? direction, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
|
||||
|
||||
var trimmedSlug = slug.Trim();
|
||||
var segments = new List<string>();
|
||||
if (page is > 0)
|
||||
{
|
||||
segments.Add($"page={page.Value}");
|
||||
}
|
||||
|
||||
if (pageSize is > 0)
|
||||
{
|
||||
segments.Add($"pageSize={pageSize.Value}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sort))
|
||||
{
|
||||
segments.Add($"sort={Uri.EscapeDataString(sort.Trim())}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(direction))
|
||||
{
|
||||
segments.Add($"dir={Uri.EscapeDataString(direction.Trim())}");
|
||||
}
|
||||
|
||||
var path = $"api/catalog/categories/{trimmedSlug}/models";
|
||||
if (segments.Count > 0)
|
||||
{
|
||||
path = $"{path}?{string.Join('&', segments)}";
|
||||
}
|
||||
|
||||
using var response = await httpClient.GetAsync(path, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GetCategoryModels.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Category products response payload was empty.");
|
||||
}
|
||||
|
||||
public async Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
|
||||
|
||||
using var response = await httpClient.GetAsync($"api/catalog/products/{slug.Trim()}", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<GetProductDetail.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Product response payload was empty.");
|
||||
}
|
||||
|
||||
public Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken) =>
|
||||
Quote(request, cancellationToken);
|
||||
|
||||
public async Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var response = await httpClient.PostAsJsonAsync("api/catalog/price-quotes", request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new RemoteProblemException(response.StatusCode, body);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<QuotePrice.Response>(cancellationToken: cancellationToken)
|
||||
?? throw new InvalidOperationException("Quote response payload was empty.");
|
||||
}
|
||||
}
|
||||
26
Prefab.Shared/Catalog/Products/QuotePrice.cs
Normal file
26
Prefab.Shared/Catalog/Products/QuotePrice.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Prefab.Shared.Catalog.Products;
|
||||
|
||||
public abstract class QuotePrice
|
||||
{
|
||||
public sealed record Request(
|
||||
Guid? ProductId,
|
||||
string? Sku,
|
||||
IReadOnlyList<Selection> Selections)
|
||||
{
|
||||
public IReadOnlyList<Selection> Selections { get; init; } = Selections ?? Array.Empty<Selection>();
|
||||
}
|
||||
|
||||
public sealed record Selection(
|
||||
Guid OptionDefinitionId,
|
||||
Guid? OptionValueId,
|
||||
decimal? NumericValue);
|
||||
|
||||
public sealed record Response(
|
||||
decimal UnitPrice,
|
||||
IReadOnlyList<QuoteBreakdown> Breakdown);
|
||||
|
||||
public sealed record QuoteBreakdown(
|
||||
string Option,
|
||||
string Chosen,
|
||||
decimal Delta);
|
||||
}
|
||||
11
Prefab.Shared/ModuleClientOptions.cs
Normal file
11
Prefab.Shared/ModuleClientOptions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Prefab.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration options for a module client, including transport selection and base address settings.
|
||||
/// </summary>
|
||||
public sealed class ModuleClientOptions
|
||||
{
|
||||
public ModuleClientTransport Transport { get; set; } = ModuleClientTransport.InProcess;
|
||||
|
||||
public Uri? BaseAddress { get; set; }
|
||||
}
|
||||
21
Prefab.Shared/ModuleClientTransport.cs
Normal file
21
Prefab.Shared/ModuleClientTransport.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Prefab.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the transport mechanism used by a module client to communicate with its host or services.
|
||||
/// </summary>
|
||||
/// <remarks>Use this enumeration to select the appropriate transport for module client operations. The choice of
|
||||
/// transport may affect performance, compatibility, and deployment scenarios. For example, 'InProcess' is typically
|
||||
/// used when the client and host run within the same process, while 'Http' enables communication over HTTP, which may
|
||||
/// be required for remote or cross-process scenarios.</remarks>
|
||||
public enum ModuleClientTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the operation or component runs within the current process.
|
||||
/// </summary>
|
||||
InProcess,
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for working with HTTP protocols, requests, and responses.
|
||||
/// </summary>
|
||||
Http
|
||||
}
|
||||
13
Prefab.Shared/Prefab.Shared.csproj
Normal file
13
Prefab.Shared/Prefab.Shared.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Prefab.Base\Prefab.Base.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
Prefab.Shared/RemoteProblemException.cs
Normal file
15
Prefab.Shared/RemoteProblemException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Prefab.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a non-success HTTP response returned by a downstream service.
|
||||
/// </summary>
|
||||
public sealed class RemoteProblemException(HttpStatusCode statusCode, string? responseBody)
|
||||
: Exception($"Remote request failed with status code {(int)statusCode}.")
|
||||
{
|
||||
public HttpStatusCode StatusCode { get; } = statusCode;
|
||||
|
||||
public string? ResponseBody { get; } = responseBody;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user