Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d229ceb
feat: Implement Dynamic Content Routing and Storage System for Forgin…
samtrion Jan 25, 2026
8b132b4
fix: update post-execution steps for task verification and dependency…
samtrion Jan 25, 2026
df54dba
chore: Implement feature X to enhance user experience and optimize pe…
samtrion Jan 25, 2026
8955560
refactor: streamline AI agent instructions and task workflow in imple…
samtrion Jan 25, 2026
154af10
feat(extensibility): add core contracts for routing and storage
samtrion Jan 25, 2026
856730e
fix: update status to 'In progress' and reflect task completion in im…
samtrion Jan 25, 2026
602634f
feat(routing): add dynamic content routing builders
samtrion Jan 25, 2026
c3a6305
docs: update task table formatting and ensure consistent date represe…
samtrion Jan 25, 2026
22cab31
feat(content): implement Phase 3 content parsing infrastructure
samtrion Jan 25, 2026
878d877
docs(parsing): Updated Phase 3
samtrion Jan 25, 2026
769fd9c
feat(storage): implement Phase 4 storage
samtrion Jan 25, 2026
840f1f6
feat(routing): implement culture resolution and fallback (Phase 5)
samtrion Jan 25, 2026
c42c511
feat(routing): implement route resolution and matching (Phase 6)
samtrion Jan 25, 2026
52cdda8
fix(tests): update tests to use expression-bodied members and remove …
samtrion Jan 25, 2026
42c235c
fix(routing): correct culture handling in routing components
samtrion Jan 25, 2026
ceec589
feat(storage): complete Phase 4 - storage provider implementations
samtrion Jan 25, 2026
f4c10a0
feat(pagination): implement Phase 7 - pagination support
samtrion Jan 25, 2026
5523f8b
feat(components): implement Phase 11 - component integration
samtrion Jan 26, 2026
337bf53
feat(storage,validation): implement Phases 8-10 - publishing workflow…
samtrion Jan 26, 2026
70c8bac
docs(plan): update last_updated timestamp
samtrion Jan 26, 2026
c93dd22
refactor(components): convert ForgingRouter and ForgingRouteView to c…
samtrion Jan 26, 2026
3233692
chore(components): add ForgingRouter and ForgingRouteView for dynamic…
samtrion Jan 26, 2026
4ffa233
feat(storage): add Azure Blob Storage package
samtrion Jan 26, 2026
da1655a
test(core): add comprehensive slug validation tests
samtrion Jan 26, 2026
1456e81
docs(plan): update timestamp for phases 12-13 completion
samtrion Jan 26, 2026
649f1c4
fix(storage): resolve Azure Blob Storage build errors
samtrion Jan 26, 2026
ef5a5e2
test(azureblob): add unit tests for Azure Blob Storage options and bu…
samtrion Jan 26, 2026
d25aec8
refactor: using directives and improve code consistency
samtrion Jan 26, 2026
e5c7af8
fix(routing): Added missing parts for ForgingRouter and ContentRouteH…
samtrion Jan 26, 2026
3855b0b
test(content): add comprehensive parsing tests and fix validation
samtrion Jan 26, 2026
692fb1a
test(routing): add comprehensive routing unit tests
samtrion Jan 26, 2026
2550602
test(routing): add unit tests for RoutingBuilder configuration and se…
samtrion Jan 26, 2026
0bf1df4
docs(plan): mark Phase 16 & 17 culture and pagination tests complete
samtrion Jan 26, 2026
98f9ee4
test(validation): add comprehensive validation unit tests
samtrion Jan 26, 2026
e3e17b2
test(integration): add comprehensive integration tests for routing an…
samtrion Jan 26, 2026
689b029
refactor: separate configuration classes into individual files
samtrion Jan 26, 2026
f47646f
refactor: extract AzuriteFixture into separate file
samtrion Jan 26, 2026
40119bc
refactor: adopt TUnit ClassDataSource pattern for Azurite fixture
samtrion Jan 26, 2026
820819c
test(azurite): Fixed Fixture and tests running
samtrion Jan 26, 2026
19d0a42
refactor: remove unnecessary pragma warnings and improve string inter…
samtrion Jan 28, 2026
c719559
refactor: enhance logging and improve code structure in various services
samtrion Jan 28, 2026
75be3c4
refactor: enhance error handling and improve method documentation in …
samtrion Jan 28, 2026
7957500
refactor: improve code structure and readability in various component…
samtrion Jan 28, 2026
8e8ea05
refactor: remove duplicated Aspire.Hosting.Testing package version fr…
samtrion Jan 28, 2026
7e738d8
refactor: update Testcontainers.Azurite package version to 4.10.0
samtrion Jan 28, 2026
dab912a
test(routing): add missing router component tests
samtrion Jan 28, 2026
dd961a2
docs(plan): update Phase 11 and Phase 18 completion status
samtrion Jan 28, 2026
3215156
refactor(tests): update TestResolvedContent to be a partial class and…
samtrion Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/agents/implementation-plan.agent.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
description: "Generate an implementation plan for new features or refactoring existing code."
name: "Implementation Plan Generation Mode"
tools: ['vscode', 'execute', 'read', 'edit', 'search', 'web', 'agent', 'gitkraken/*', 'fetch/*', 'nuget-server/*', 'cognitionai/deepwiki/*', 'github/*', 'microsoftdocs/mcp/*', 'todo']
tools:
['vscode', 'execute', 'read', 'edit', 'search', 'web', 'agent', 'fetch/*', 'nuget-server/*', 'cognitionai/deepwiki/*', 'github/*', 'microsoftdocs/mcp/*', 'todo']
---

# Implementation Plan Generation Mode
Expand Down
8 changes: 7 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@
<GlobalPackageReference Include="SonarAnalyzer.CSharp" Version="10.18.0.131500" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.1.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.22.0" />
<PackageVersion Include="bunit" Version="1.34.0" />
<PackageVersion Include="Markdig" Version="0.41.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.2.0" />
<PackageVersion Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageVersion Include="NetEvolve.Extensions.TUnit" Version="3.5.3" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.1.0" />
<PackageVersion Include="Testcontainers.Azurite" Version="4.10.0" />
<PackageVersion Include="TUnit" Version="1.12.65" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions ForgingBlazor.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
</Folder>
<Folder Name="/src/">
<Project Path="src/ForgingBlazor.Extensibility/ForgingBlazor.Extensibility.csproj" />
<Project Path="src/ForgingBlazor.Storage.AzureBlob/ForgingBlazor.Storage.AzureBlob.csproj" />
<Project Path="src/ForgingBlazor/ForgingBlazor.csproj" />
<Project Path="src/Xample.AppHost/Xample.AppHost.csproj" />
<Project Path="src/Xample.ServiceDefaults/Xample.ServiceDefaults.csproj" />
Expand All @@ -30,6 +31,7 @@
<Folder Name="/tests/">
<Project Path="tests/ForgingBlazor.Extensibility.Tests.Integration/ForgingBlazor.Extensibility.Tests.Integration.csproj" />
<Project Path="tests/ForgingBlazor.Extensibility.Tests.Unit/ForgingBlazor.Extensibility.Tests.Unit.csproj" />
<Project Path="tests/ForgingBlazor.Storage.AzureBlob.Tests.Unit/ForgingBlazor.Storage.AzureBlob.Tests.Unit.csproj" />
<Project Path="tests/ForgingBlazor.Tests.Integration/ForgingBlazor.Tests.Integration.csproj" />
<Project Path="tests/ForgingBlazor.Tests.Unit/ForgingBlazor.Tests.Unit.csproj" />
<Project Path="tests/Xample.AppHost.Tests.Integration/Xample.AppHost.Tests.Integration.csproj" />
Expand Down
1,085 changes: 1,085 additions & 0 deletions plan/feature-dynamic-content-routing-1.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions src/ForgingBlazor.Extensibility/Check.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,88 @@ pathSegment is not null
&& (Defaults.SegmentLengthMinimum <= pathSegment.Length)
== (pathSegment.Length <= Defaults.SegmentLengthMaximum)
&& pathSegment.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_');

/// <summary>
/// Determines whether the specified slug value matches the required slug pattern.
/// </summary>
/// <param name="slug">The slug to validate.</param>
/// <returns><see langword="true"/> when the slug is valid; otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// A valid slug must:
/// <list type="bullet">
/// <item><description>Be between 3 and 70 characters (inclusive)</description></item>
/// <item><description>Start and end with an ASCII letter</description></item>
/// <item><description>Contain only ASCII letters, digits, or single hyphen characters (<c>-</c>)</description></item>
/// <item><description>Not contain consecutive hyphen characters</description></item>
/// </list>
/// </remarks>
internal static bool IsValidSlug(string? slug)
{
if (slug is null)
{
return false;
}

var length = slug.Length;
if ((Defaults.SegmentLengthMinimum <= length) != (length <= Defaults.SegmentLengthMaximum))
{
return false;
}

if (!IsAsciiLetter(slug[0]) || !IsAsciiLetter(slug[^1]))
{
return false;
}

var previousWasHyphen = false;
for (var index = 0; index < length; index++)
{
var character = slug[index];
if (IsAsciiLetter(character) || IsAsciiDigit(character))
{
previousWasHyphen = false;
continue;
}

if (character == '-')
{
if (previousWasHyphen)
{
return false;
}

previousWasHyphen = true;
continue;
}

return false;
}

return true;
}

/// <summary>
/// Validates the specified slug and throws an <see cref="ArgumentException"/> if it is invalid.
/// </summary>
/// <param name="slug">The slug to validate.</param>
/// <param name="parameterName">The optional parameter name for exception reporting.</param>
/// <returns>The validated slug.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="slug"/> is invalid.</exception>
internal static string ValidateSlug(string? slug, string? parameterName = null)
{
if (IsValidSlug(slug))
{
return slug!;
}

var name = string.IsNullOrWhiteSpace(parameterName) ? nameof(slug) : parameterName;
throw new ArgumentException(
$"The slug '{slug}' is not valid. Slugs must start and end with a letter, may contain letters, digits, and single hyphens, and must be between {Defaults.SegmentLengthMinimum} and {Defaults.SegmentLengthMaximum} characters.",
name
);
}

private static bool IsAsciiLetter(char value) => ('A' <= value && value <= 'Z') || ('a' <= value && value <= 'z');

private static bool IsAsciiDigit(char value) => '0' <= value && value <= '9';
}
56 changes: 56 additions & 0 deletions src/ForgingBlazor.Extensibility/Content/ContentDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace NetEvolve.ForgingBlazor;

using System;

/// <summary>
/// Represents the core, minimal description of a content item used by the dynamic content system.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><description><see cref="Title"/>: A human-friendly title for the content.</description></item>
/// <item><description><see cref="Slug"/>: A URL-safe identifier used for routing (e.g., "getting-started").</description></item>
/// <item><description><see cref="PublishedDate"/>: The date and time the content is considered published.</description></item>
/// <item><description><see cref="Draft"/>: Indicates whether the content is a draft and should not be publicly visible.</description></item>
/// <item><description><see cref="ExpiredAt"/>: The optional date and time when the content expires and should no longer be served.</description></item>
/// <item><description><see cref="Body"/>: The Markdown body of the content, if any.</description></item>
/// <item><description><see cref="BodyHtml"/>: The rendered HTML body of the content, if available.</description></item>
/// </list>
/// This type is a POCO and intentionally contains no behavior.
/// </remarks>
public class ContentDescriptor
{
/// <summary>
/// A human-friendly title for the content.
/// </summary>
public required string Title { get; set; }

/// <summary>
/// A URL-safe identifier used for routing (e.g., "getting-started").
/// </summary>
public required string Slug { get; set; }

/// <summary>
/// The date and time the content is considered published.
/// </summary>
public DateTimeOffset PublishedDate { get; set; }

/// <summary>
/// Indicates whether the content is a draft and should not be publicly visible.
/// </summary>
public bool Draft { get; set; }

/// <summary>
/// The optional date and time when the content expires and should no longer be served.
/// </summary>
public DateTimeOffset? ExpiredAt { get; set; }

/// <summary>
/// The Markdown body of the content, if any.
/// </summary>
public string? Body { get; set; }

/// <summary>
/// The rendered HTML body of the content, if available.
/// </summary>
public string? BodyHtml { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace NetEvolve.ForgingBlazor;

using System;

/// <summary>
/// Represents an exception thrown when content validation fails.
/// </summary>
public sealed class ContentValidationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentValidationException"/> class.
/// </summary>
public ContentValidationException()
: base() { }

/// <summary>
/// Initializes a new instance of the <see cref="ContentValidationException"/> class.
/// </summary>
/// <param name="message">The error message explaining the validation failure.</param>
public ContentValidationException(string message)
: base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="ContentValidationException"/> class.
/// </summary>
/// <param name="message">The error message explaining the validation failure.</param>
/// <param name="innerException">The exception that caused this validation exception.</param>
public ContentValidationException(string message, Exception innerException)
: base(message, innerException) { }

/// <summary>
/// Initializes a new instance of the <see cref="ContentValidationException"/> class.
/// </summary>
/// <param name="fieldName">The name of the field that failed validation.</param>
/// <param name="expectedType">The expected type for the field value.</param>
/// <param name="actualValue">The actual value that was provided.</param>
public ContentValidationException(string fieldName, Type expectedType, object? actualValue)
: base(CreateMessage(fieldName, expectedType, actualValue))
{
ArgumentNullException.ThrowIfNull(fieldName);
ArgumentNullException.ThrowIfNull(expectedType);

FieldName = fieldName;
ExpectedType = expectedType;
ActualValue = actualValue;
}

private static string CreateMessage(string fieldName, Type expectedType, object? actualValue)
{
ArgumentNullException.ThrowIfNull(expectedType);
return $"Content validation failed for field '{fieldName}'. Expected type '{expectedType.Name}', but received value: {actualValue ?? "null"}.";
}

/// <summary>
/// Gets the name of the field that failed validation.
/// </summary>
public string? FieldName { get; }

/// <summary>
/// Gets the expected type for the field value.
/// </summary>
public Type? ExpectedType { get; }

/// <summary>
/// Gets the actual value that was provided.
/// </summary>
public object? ActualValue { get; }
}
24 changes: 24 additions & 0 deletions src/ForgingBlazor.Extensibility/Content/IMetadataConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace NetEvolve.ForgingBlazor;

/// <summary>
/// Configures metadata extensions for content descriptors and pages.
/// </summary>
public interface IMetadataConfiguration
{
/// <summary>
/// Adds an extensible metadata field.
/// </summary>
/// <typeparam name="T">The metadata field type.</typeparam>
/// <param name="fieldName">The field name (case-insensitive recommendation).</param>
/// <returns>The <see cref="IMetadataConfiguration"/> for chaining.</returns>
IMetadataConfiguration ExtendWith<T>(string fieldName) => ExtendWith<T>(fieldName, default!);

/// <summary>
/// Adds an extensible metadata field with a default value.
/// </summary>
/// <typeparam name="T">The metadata field type.</typeparam>
/// <param name="fieldName">The field name (case-insensitive recommendation).</param>
/// <param name="defaultValue">The default value to apply when not provided.</param>
/// <returns>The <see cref="IMetadataConfiguration"/> for chaining.</returns>
IMetadataConfiguration ExtendWith<T>(string fieldName, T defaultValue);
}
33 changes: 33 additions & 0 deletions src/ForgingBlazor.Extensibility/Content/ResolvedContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma warning disable CA1056 // CanonicalUrl is intentionally a string per contract requirements
namespace NetEvolve.ForgingBlazor;

using System.Collections.Generic;
using System.Globalization;

/// <summary>
/// Represents a content item resolved for a specific culture and route context.
/// </summary>
/// <typeparam name="TDescriptor">A content descriptor type derived from <see cref="ContentDescriptor"/>.</typeparam>
public class ResolvedContent<TDescriptor>
where TDescriptor : ContentDescriptor
{
/// <summary>
/// The descriptor containing the content metadata and body.
/// </summary>
public required TDescriptor Descriptor { get; init; }

/// <summary>
/// The culture used for resolution.
/// </summary>
public required CultureInfo Culture { get; init; }

/// <summary>
/// The canonical URL for this content instance in the resolved culture.
/// </summary>
public required string CanonicalUrl { get; init; }

/// <summary>
/// Route values associated with the resolution context (e.g., segment, pagination, parameters).
/// </summary>
public required IReadOnlyDictionary<string, object?> RouteValues { get; init; }
}
6 changes: 6 additions & 0 deletions src/ForgingBlazor.Extensibility/Defaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
/// </summary>
public static class Defaults
{
/// <summary>
/// Defines the default page size for pagination operations.
/// </summary>
/// <value>The default page size is 10.</value>
public const int PageSizeDefault = 10;

/// <summary>
/// Defines the minimum allowed page size for pagination operations.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
namespace NetEvolve.ForgingBlazor;

using Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Defines the contract for building a ForgingBlazor application.
/// </summary>
public interface IForgingBlazorApplicationBuilder
{
/// <summary>
/// Gets the service collection for configuring application services.
/// </summary>
IServiceCollection Services { get; }

/// <summary>
/// Builds and returns a configured <see cref="IForgingBlazorApplication"/> instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace NetEvolve.ForgingBlazor;

/// <summary>
/// Configures pagination behavior for routed content.
/// </summary>
public interface IPaginationConfiguration
{
/// <summary>
/// Sets the number of items per page.
/// </summary>
/// <param name="size">The page size (must be a positive integer).</param>
/// <returns>The <see cref="IPaginationConfiguration"/> for chaining.</returns>
IPaginationConfiguration PageSize(int size = Defaults.PageSizeDefault);

/// <summary>
/// Sets the URL format for pagination segments.
/// </summary>
/// <param name="format">The pagination URL format.</param>
/// <param name="prefix">The optional prefix for pagination segments (used with <see cref="PaginationUrlFormat.Prefixed"/>).</param>
/// <returns>The <see cref="IPaginationConfiguration"/> for chaining.</returns>
IPaginationConfiguration UrlFormat(PaginationUrlFormat format = PaginationUrlFormat.Numeric, string? prefix = null);
}
Loading
Loading