Init
This commit is contained in:
215
Prefab.Web/UrlPolicy/UrlPolicy.cs
Normal file
215
Prefab.Web/UrlPolicy/UrlPolicy.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Prefab.Web.UrlPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Provides centralized helpers for building canonical catalog URLs used by the BFF.
|
||||
/// </summary>
|
||||
public static class UrlPolicy
|
||||
{
|
||||
public const int DefaultPage = 1;
|
||||
public const int DefaultPageSize = 24;
|
||||
public const string DefaultSort = "name:asc";
|
||||
public const string DefaultView = "grid";
|
||||
|
||||
private static readonly int[] AllowedPageSizes = [12, 24, 48];
|
||||
|
||||
/// <summary>
|
||||
/// Builds a canonical catalog URL for navigating to a category landing page.
|
||||
/// </summary>
|
||||
/// <param name="slug">The category slug to embed in the query string.</param>
|
||||
public static string BrowseCategory(string slug)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
|
||||
return $"/catalog/category?category-slug={Uri.EscapeDataString(slug)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a canonical catalog URL for navigating to the products grid for a category.
|
||||
/// </summary>
|
||||
/// <param name="categorySlug">The slug of the category whose products should be displayed.</param>
|
||||
public static string BrowseProducts(string categorySlug)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(categorySlug);
|
||||
return $"/catalog/products?category-slug={Uri.EscapeDataString(categorySlug)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a canonical catalog URL for navigating to a specific product detail page.
|
||||
/// </summary>
|
||||
/// <param name="productSlug">The slug of the product.</param>
|
||||
public static string BrowseProduct(string productSlug)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(productSlug);
|
||||
return $"/catalog/product?product-slug={Uri.EscapeDataString(productSlug)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a canonical catalog URL for the category browsing experience including paging state.
|
||||
/// </summary>
|
||||
public static string Category(
|
||||
string slug,
|
||||
int page = DefaultPage,
|
||||
int pageSize = DefaultPageSize,
|
||||
string? sort = null,
|
||||
string? view = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
|
||||
|
||||
var normalizedPage = NormalizePage(page);
|
||||
var normalizedPageSize = NormalizePageSize(pageSize);
|
||||
var normalizedSort = NormalizeSort(sort);
|
||||
var normalizedView = NormalizeView(view);
|
||||
|
||||
var parameters = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["slug"] = slug,
|
||||
["page"] = normalizedPage.ToString(CultureInfo.InvariantCulture),
|
||||
["pageSize"] = normalizedPageSize.ToString(CultureInfo.InvariantCulture),
|
||||
["sort"] = normalizedSort,
|
||||
["view"] = normalizedView
|
||||
};
|
||||
|
||||
return QueryHelpers.AddQueryString("/catalog/category", parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the catalog category browse URL parameters.
|
||||
/// </summary>
|
||||
public static bool TryParseCategory(
|
||||
Uri uri,
|
||||
out string slug,
|
||||
out int page,
|
||||
out int pageSize,
|
||||
out string sort,
|
||||
out string view)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uri);
|
||||
|
||||
slug = string.Empty;
|
||||
page = DefaultPage;
|
||||
pageSize = DefaultPageSize;
|
||||
sort = DefaultSort;
|
||||
view = DefaultView;
|
||||
|
||||
var queryValues = ParseQuery(uri);
|
||||
if (!queryValues.TryGetValue("slug", out var slugValues))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidateSlug = slugValues.ToString();
|
||||
if (string.IsNullOrWhiteSpace(candidateSlug))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
slug = candidateSlug;
|
||||
page = NormalizePage(ParseInt(queryValues, "page", DefaultPage));
|
||||
pageSize = NormalizePageSize(ParseInt(queryValues, "pageSize", DefaultPageSize));
|
||||
sort = NormalizeSort(queryValues.TryGetValue("sort", out var sortValues) ? sortValues.ToString() : null);
|
||||
view = NormalizeView(queryValues.TryGetValue("view", out var viewValues) ? viewValues.ToString() : null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a canonical catalog URL for browsing products without a category context.
|
||||
/// </summary>
|
||||
public static string Products(
|
||||
int page = DefaultPage,
|
||||
int pageSize = DefaultPageSize,
|
||||
string? sort = null,
|
||||
string? view = null)
|
||||
{
|
||||
var normalizedPage = NormalizePage(page);
|
||||
var normalizedPageSize = NormalizePageSize(pageSize);
|
||||
var normalizedSort = NormalizeSort(sort);
|
||||
var normalizedView = NormalizeView(view);
|
||||
|
||||
var parameters = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["page"] = normalizedPage.ToString(CultureInfo.InvariantCulture),
|
||||
["pageSize"] = normalizedPageSize.ToString(CultureInfo.InvariantCulture),
|
||||
["sort"] = normalizedSort,
|
||||
["view"] = normalizedView
|
||||
};
|
||||
|
||||
return QueryHelpers.AddQueryString("/catalog/products", parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the products browse URL parameters.
|
||||
/// </summary>
|
||||
public static bool TryParseProducts(
|
||||
Uri uri,
|
||||
out int page,
|
||||
out int pageSize,
|
||||
out string sort,
|
||||
out string view)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uri);
|
||||
|
||||
page = DefaultPage;
|
||||
pageSize = DefaultPageSize;
|
||||
sort = DefaultSort;
|
||||
view = DefaultView;
|
||||
|
||||
var queryValues = ParseQuery(uri);
|
||||
|
||||
page = NormalizePage(ParseInt(queryValues, "page", DefaultPage));
|
||||
pageSize = NormalizePageSize(ParseInt(queryValues, "pageSize", DefaultPageSize));
|
||||
sort = NormalizeSort(queryValues.TryGetValue("sort", out var sortValues) ? sortValues.ToString() : null);
|
||||
view = NormalizeView(queryValues.TryGetValue("view", out var viewValues) ? viewValues.ToString() : null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IDictionary<string, StringValues> ParseQuery(Uri uri)
|
||||
{
|
||||
var querySegment = uri.IsAbsoluteUri ? uri.Query : uri.OriginalString;
|
||||
if (string.IsNullOrEmpty(querySegment))
|
||||
{
|
||||
return new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (!uri.IsAbsoluteUri)
|
||||
{
|
||||
var questionIndex = querySegment.IndexOf('?', StringComparison.Ordinal);
|
||||
querySegment = questionIndex >= 0 ? querySegment[questionIndex..] : string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(querySegment))
|
||||
{
|
||||
return new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return QueryHelpers.ParseQuery(querySegment);
|
||||
}
|
||||
|
||||
private static int ParseInt(IDictionary<string, StringValues> values, string key, int fallback)
|
||||
{
|
||||
if (values.TryGetValue(key, out var raw) &&
|
||||
int.TryParse(raw.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static int NormalizePage(int page) => page < 1 ? DefaultPage : page;
|
||||
|
||||
private static int NormalizePageSize(int pageSize) =>
|
||||
AllowedPageSizes.Contains(pageSize) ? pageSize : DefaultPageSize;
|
||||
|
||||
private static string NormalizeSort(string? sort) =>
|
||||
string.IsNullOrWhiteSpace(sort) ? DefaultSort : sort.Trim();
|
||||
|
||||
private static string NormalizeView(string? view) =>
|
||||
string.IsNullOrWhiteSpace(view) ? DefaultView : view.Trim().ToLowerInvariant();
|
||||
}
|
||||
Reference in New Issue
Block a user