Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/CrestApps.Core.Docs/docs/changelog/1.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ page will be updated as changes land after 1.0.0.
## Highlights

- upgrades the framework's dependency baseline, including YesSql 6.0 (new `ISession.SaveAsync` signature), the Model Context Protocol 2.0 packages, the GitHub Copilot SDK 1.0.8 (new `PermissionsApi.SetAllowAllAsync` mode-based API), Anthropic 12.39.0, OllamaSharp 5.4.30, the .NET 10.0.10 runtime/extension packages, and the `Microsoft.Extensions.AI` 10.8.3 packages

## Fixes

- fixes AI document citation links throwing `System.InvalidOperationException: The following endpoints with a duplicate endpoint name were found` in multi-tenant hosts. `DocumentAIReferenceLinkResolver` no longer resolves the download link by endpoint name (which forced ASP.NET Core to validate global endpoint-name uniqueness and failed when other modules registered the same named API endpoints across tenants); it now builds the path directly from the static `DownloadAIDocument.RoutePattern`, respecting the request `PathBase`.
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ public static class DownloadAIDocument
{
public const string DefaultRouteName = "DownloadAIDocument";

/// <summary>
/// The static route pattern mapped for the AI document download endpoint.
/// </summary>
public const string RoutePattern = "ai/documents/{documentId}/download";

/// <summary>
/// Adds the shared AI document download endpoint used by citation links.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="routeName">The route name.</param>
public static IEndpointRouteBuilder AddDownloadAIDocumentEndpoint(this IEndpointRouteBuilder builder, string routeName = DefaultRouteName)
{
var endpoint = builder.MapGet("ai/documents/{documentId}/download", HandleAsync);
var endpoint = builder.MapGet(RoutePattern, HandleAsync);

if (!string.IsNullOrEmpty(routeName))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using CrestApps.Core.AI.Documents.Endpoints;
using CrestApps.Core.AI.Profiles;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace CrestApps.Core.AI.Documents.Services;

Expand All @@ -10,19 +9,14 @@ namespace CrestApps.Core.AI.Documents.Services;
/// </summary>
public sealed class DocumentAIReferenceLinkResolver : IAIReferenceLinkResolver
{
private readonly LinkGenerator _linkGenerator;
private readonly IHttpContextAccessor _httpContextAccessor;

/// <summary>
/// Initializes a new instance of the <see cref="DocumentAIReferenceLinkResolver"/> class.
/// </summary>
/// <param name="linkGenerator">The link generator.</param>
/// <param name="httpContextAccessor">The http context accessor.</param>
public DocumentAIReferenceLinkResolver(
LinkGenerator linkGenerator,
IHttpContextAccessor httpContextAccessor)
public DocumentAIReferenceLinkResolver(IHttpContextAccessor httpContextAccessor)
{
_linkGenerator = linkGenerator;
_httpContextAccessor = httpContextAccessor;
}

Expand All @@ -38,12 +32,17 @@ public string ResolveLink(string referenceId, IDictionary<string, object> metada
return null;
}

return _linkGenerator.GetPathByName(
_httpContextAccessor.HttpContext,
DownloadAIDocument.DefaultRouteName,
new RouteValueDictionary
{
["documentId"] = referenceId,
});
// Build the path directly from the static route pattern instead of resolving by endpoint
// name. Name-based link generation forces ASP.NET Core to validate that every endpoint name
// is globally unique, which throws in multi-tenant hosts where other modules register the
// same named API endpoints across tenants.
var relativePath = "/" + DownloadAIDocument.RoutePattern.Replace(
"{documentId}",
Uri.EscapeDataString(referenceId),
StringComparison.Ordinal);

var pathBase = _httpContextAccessor.HttpContext?.Request.PathBase ?? PathString.Empty;

return pathBase.Add(relativePath).Value;
}
}
Loading