File: Mcp\ApiDocs\ApiDocsIndexServiceTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Documentation;
using Aspire.Cli.Documentation.ApiDocs;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Configuration;
 
namespace Aspire.Cli.Tests.Documentation.ApiDocs;
 
public class ApiDocsIndexServiceTests
{
    private const string CSharpMethodsUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testtype/methods";
    private const string CSharpTypeUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testtype";
    private const string CSharpWithEnvironmentMethodsUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods";
    private const string CSharpWithEnvironmentTypeUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions";
 
    [Fact]
    public async Task ListAsync_BuildsHierarchyForBothLanguages()
    {
        var service = CreateService();
 
        var csharpRoots = await service.ListAsync("csharp");
        var typeScriptRoots = await service.ListAsync("typescript");
        var csharpChildren = await service.ListAsync("csharp/aspire.test.package");
        var csharpTypeChildren = await service.ListAsync("csharp/aspire.test.package/testtype");
        var typeScriptChildren = await service.ListAsync("typescript/aspire.hosting.test");
 
        Assert.Collection(
            csharpRoots,
            item =>
            {
                Assert.Equal("csharp/aspire.test.package", item.Id);
                Assert.Equal(ApiReferenceKinds.Package, item.Kind);
            });
 
        Assert.Collection(
            typeScriptRoots,
            item =>
            {
                Assert.Equal("typescript/aspire.hosting.test", item.Id);
                Assert.Equal(ApiReferenceKinds.Module, item.Kind);
            });
 
        Assert.Collection(
            csharpChildren,
            item =>
            {
                Assert.Equal("csharp/aspire.test.package/testtype", item.Id);
                Assert.Equal(ApiReferenceKinds.Type, item.Kind);
            });
 
        Assert.Collection(
            csharpTypeChildren,
            item =>
            {
                Assert.Equal("csharp/aspire.test.package/testtype/methods", item.Id);
                Assert.Equal(ApiReferenceKinds.MemberGroup, item.Kind);
            });
 
        Assert.Collection(
            typeScriptChildren,
            item =>
            {
                Assert.Equal("typescript/aspire.hosting.test/testresource", item.Id);
                Assert.Equal(ApiReferenceKinds.Symbol, item.Kind);
            });
    }
 
    [Fact]
    public async Task ListAsync_BuildsHierarchyForGo()
    {
        var service = CreateGoAndTypeScriptService();
 
        var roots = await service.ListAsync("go");
        var moduleChildren = await service.ListAsync("go/aspire.hosting");
        var itemChildren = await service.ListAsync("go/aspire.hosting/containerresource");
        var redisChildren = await service.ListAsync("go/aspire.hosting.redis");
 
        Assert.Collection(
            roots,
            item =>
            {
                Assert.Equal("go/aspire.hosting", item.Id);
                Assert.Equal(ApiReferenceLanguages.Go, item.Language);
                Assert.Equal(ApiReferenceKinds.Module, item.Kind);
                Assert.Equal("go", item.ParentId);
            },
            item =>
            {
                Assert.Equal("go/aspire.hosting.redis", item.Id);
                Assert.Equal(ApiReferenceLanguages.Go, item.Language);
                Assert.Equal(ApiReferenceKinds.Module, item.Kind);
                Assert.Equal("go", item.ParentId);
            });
 
        Assert.Collection(
            moduleChildren,
            item =>
            {
                Assert.Equal("go/aspire.hosting/containerresource", item.Id);
                Assert.Equal(ApiReferenceLanguages.Go, item.Language);
                Assert.Equal(ApiReferenceKinds.Symbol, item.Kind);
                Assert.Equal("go/aspire.hosting", item.ParentId);
            },
            item =>
            {
                Assert.Equal("go/aspire.hosting/environmentcallbackcontext", item.Id);
                Assert.Equal(ApiReferenceLanguages.Go, item.Language);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("go/aspire.hosting", item.ParentId);
            });
 
        Assert.Collection(
            itemChildren,
            item =>
            {
                Assert.Equal("go/aspire.hosting/containerresource/withendpoint", item.Id);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("go/aspire.hosting/containerresource", item.ParentId);
            },
            item =>
            {
                Assert.Equal("go/aspire.hosting/containerresource/withenvironment", item.Id);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("go/aspire.hosting/containerresource", item.ParentId);
            });
 
        Assert.Collection(
            redisChildren,
            item =>
            {
                Assert.Equal("go/aspire.hosting.redis/addredis", item.Id);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("go/aspire.hosting.redis", item.ParentId);
            });
    }
 
    [Fact]
    public async Task ListAsync_ClassifiesGoAndTypeScriptItemsIndependently()
    {
        var service = CreateGoAndTypeScriptService();
 
        var goChildren = await service.ListAsync("go/aspire.hosting");
        var typeScriptChildren = await service.ListAsync("typescript/aspire.hosting");
 
        var goItem = Assert.Single(goChildren, item => item.Id == "go/aspire.hosting/containerresource");
        Assert.Equal(ApiReferenceKinds.Symbol, goItem.Kind);
 
        var typeScriptItem = Assert.Single(typeScriptChildren);
        Assert.Equal("typescript/aspire.hosting/containerresource", typeScriptItem.Id);
        Assert.Equal(ApiReferenceKinds.Member, typeScriptItem.Kind);
    }
 
    [Fact]
    public async Task SearchAsync_RespectsLanguageFilterAndFindsDirectRouteItems()
    {
        var service = CreateService();
 
        var csharpResults = await service.SearchAsync("methods", ApiReferenceLanguages.CSharp, 10);
        var typeScriptResults = await service.SearchAsync("methods", ApiReferenceLanguages.TypeScript, 10);
 
        var result = Assert.Single(csharpResults);
        Assert.Equal("csharp/aspire.test.package/testtype/methods", result.Id);
        Assert.Equal(ApiReferenceKinds.MemberGroup, result.Kind);
        Assert.Empty(typeScriptResults);
    }
 
    [Fact]
    public async Task SearchAsync_RespectsGoLanguageFilterAndFindsDirectRouteItems()
    {
        var service = CreateGoAndTypeScriptService();
 
        var goResults = await service.SearchAsync("addredis", ApiReferenceLanguages.Go, 10);
        var typeScriptResults = await service.SearchAsync("addredis", ApiReferenceLanguages.TypeScript, 10);
        var allResults = await service.SearchAsync("addredis", topK: 10);
 
        var goResult = Assert.Single(goResults);
        Assert.Equal("go/aspire.hosting.redis/addredis", goResult.Id);
        Assert.Equal(ApiReferenceLanguages.Go, goResult.Language);
        Assert.Equal(ApiReferenceKinds.Member, goResult.Kind);
        Assert.Equal("go/aspire.hosting.redis", goResult.ParentId);
 
        var typeScriptResult = Assert.Single(typeScriptResults);
        Assert.Equal("typescript/aspire.hosting.redis/addredis", typeScriptResult.Id);
 
        Assert.Equal(2, allResults.Count);
        Assert.Contains(allResults, result => result.Id == goResult.Id);
        Assert.Contains(allResults, result => result.Id == typeScriptResult.Id);
    }
 
    [Fact]
    public async Task SearchAsync_ListsTypeScriptBeforeGoWhenScoresTie()
    {
        var service = CreateGoAndTypeScriptService();
 
        var results = await service.SearchAsync("addredis", topK: 10);
 
        // Go routes mirror TypeScript routes segment for segment, so the twins score the same.
        // The language order, not the "go/" versus "typescript/" id prefix, decides which is listed first.
        Assert.Collection(
            results,
            result => Assert.Equal("typescript/aspire.hosting.redis/addredis", result.Id),
            result => Assert.Equal("go/aspire.hosting.redis/addredis", result.Id));
        Assert.Equal(results[0].Score, results[1].Score);
    }
 
    [Theory]
    [InlineData("go")]
    [InlineData("GO")]
    [InlineData(" Go ")]
    public async Task SearchAsync_AcceptsGoLanguageFilterCaseInsensitively(string language)
    {
        var service = CreateGoAndTypeScriptService();
 
        var results = await service.SearchAsync("addredis", language, 10);
 
        var result = Assert.Single(results);
        Assert.Equal("go/aspire.hosting.redis/addredis", result.Id);
    }
 
    [Theory]
    [InlineData("golang")]
    [InlineData("rust")]
    public async Task SearchAsync_RejectsUnsupportedLanguageFilter(string language)
    {
        var service = CreateGoAndTypeScriptService();
 
        var results = await service.SearchAsync("addredis", language, 10);
 
        Assert.Empty(results);
    }
 
    [Theory]
    [InlineData("csharp", true)]
    [InlineData("typescript", true)]
    [InlineData("go", true)]
    [InlineData("GO", true)]
    [InlineData("golang", false)]
    [InlineData("rust", false)]
    [InlineData("", false)]
    public void ApiReferenceLanguages_IsSupported_MatchesKnownLanguages(string language, bool expected)
    {
        Assert.Equal(expected, ApiReferenceLanguages.IsSupported(language));
    }
 
    [Fact]
    public async Task SearchAsync_FindsMembersParsedFromGroupedMemberLinks()
    {
        var service = CreateService();
 
        var results = await service.SearchAsync("DoThing", ApiReferenceLanguages.CSharp, 10);
 
        var result = Assert.Single(results);
        Assert.Equal("csharp/aspire.test.package/testtype/methods#dothing", result.Id);
        Assert.Equal("DoThing(string)", result.Name);
        Assert.Equal(ApiReferenceKinds.Member, result.Kind);
        Assert.Equal("Does the thing.", result.Summary);
    }
 
    [Fact]
    public async Task SearchAsync_LoadsMemberIndexWhenBaseRouteHitsExist()
    {
        var service = CreateWithEnvironmentService();
 
        var results = await service.SearchAsync("WithEnv", ApiReferenceLanguages.CSharp, 10);
 
        Assert.Equal(3, results.Count);
        Assert.Contains(results, result => result.Id == "csharp/aspire.test.package/iresourcewithenvironment");
        Assert.Contains(results, result => result.Name == "WithEnvironment(IResourceBuilder<T>, string, string?)" &&
            result.Id.StartsWith("csharp/aspire.test.package/resourcebuilderextensions/methods#withenvironment-", StringComparison.Ordinal));
        Assert.Contains(results, result => result.Name == "WithEnvironment(IResourceBuilder<T>, Action<EnvironmentCallbackContext>)" &&
            result.Id.StartsWith("csharp/aspire.test.package/resourcebuilderextensions/methods#withenvironment-", StringComparison.Ordinal));
        Assert.DoesNotContain(results, result => result.Id.Contains("iresourcebuilder", StringComparison.OrdinalIgnoreCase));
    }
 
    [Fact]
    public async Task SearchAsync_PrefersTypesForBroadIdentifierQueries()
    {
        var service = CreateDistributedApplicationService();
 
        var results = await service.SearchAsync("distributedapp", ApiReferenceLanguages.CSharp, 10);
 
        Assert.NotEmpty(results);
        var firstResult = results[0];
        Assert.Equal("csharp/aspire.hosting/distributedapplication", firstResult.Id);
        Assert.Equal(ApiReferenceKinds.Type, firstResult.Kind);
        Assert.Contains(results, result => result.Id == "csharp/aspire.hosting.testing/distributedapplicationfactory");
        Assert.Contains(results, result => result.Kind == ApiReferenceKinds.Member);
    }
 
    [Fact]
    public async Task SearchAsync_LoadsOnlyNeededMemberContainers()
    {
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/alphatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/alphatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/betatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/betatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/deltatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/deltatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/epsilontype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/epsilontype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/etatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/etatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/gammatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/gammatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/iotatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/iotatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/kappatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/kappatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/thetatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/thetatype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/zetatype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/zetatype/methods/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["https://aspire.dev/reference/api/csharp/aspire.test.package/alphatype"] = """
                    # AlphaType
 
                    ## Methods
 
                    - [FindMe()](/reference/api/csharp/aspire.test.package/alphatype/methods.md#findme) : `void` -- Finds the result immediately.
                    """,
                ["https://aspire.dev/reference/api/csharp/aspire.test.package/betatype"] = """
                    # BetaType
 
                    ## Methods
 
                    - [DifferentThing()](/reference/api/csharp/aspire.test.package/betatype/methods.md#differentthing) : `void` -- Does something else.
                    """
            });
 
        var service = new ApiDocsIndexService(fetcher, new TestApiDocsCache(), new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
 
        var results = await service.SearchAsync("FindMe", ApiReferenceLanguages.CSharp, 1);
 
        var result = Assert.Single(results);
        Assert.Equal("csharp/aspire.test.package/alphatype/methods#findme", result.Id);
        Assert.Contains("https://aspire.dev/reference/api/csharp/aspire.test.package/alphatype", fetcher.RequestedPageUrls);
        Assert.DoesNotContain("https://aspire.dev/reference/api/csharp/aspire.test.package/thetatype", fetcher.RequestedPageUrls);
        Assert.DoesNotContain("https://aspire.dev/reference/api/csharp/aspire.test.package/zetatype", fetcher.RequestedPageUrls);
    }
 
    [Fact]
    public async Task ListAsync_ForMemberGroupScope_ReturnsParsedMembers()
    {
        var service = CreateService();
 
        var items = await service.ListAsync("csharp/aspire.test.package/testtype/methods");
 
        Assert.Collection(
            items,
            item =>
            {
                Assert.Equal("csharp/aspire.test.package/testtype/methods#doother", item.Id);
                Assert.Equal("DoOther()", item.Name);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("csharp/aspire.test.package/testtype/methods", item.ParentId);
            },
            item =>
            {
                Assert.Equal("csharp/aspire.test.package/testtype/methods#dothing", item.Id);
                Assert.Equal("DoThing(string)", item.Name);
                Assert.Equal(ApiReferenceKinds.Member, item.Kind);
                Assert.Equal("csharp/aspire.test.package/testtype/methods", item.ParentId);
            });
    }
 
    [Fact]
    public async Task GetAsync_ForDirectRouteItem_ReturnsRawMarkdown()
    {
        var service = CreateService();
 
        var item = await service.GetAsync("csharp/aspire.test.package/testtype/methods");
 
        Assert.NotNull(item);
        Assert.Equal("methods", item.Name);
        Assert.Equal(ApiReferenceKinds.MemberGroup, item.Kind);
        Assert.Contains("## DoThing(string) {#dothing-string}", item.Content, StringComparison.Ordinal);
        Assert.Contains("## DoOther() {#doother}", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_ForParsedMember_ReturnsAnchoredUrlAndRawMarkdown()
    {
        var service = CreateService();
 
        var item = await service.GetAsync("csharp/aspire.test.package/testtype/methods#dothing");
 
        Assert.NotNull(item);
        Assert.Equal("DoThing(string)", item.Name);
        Assert.Equal(ApiReferenceKinds.Member, item.Kind);
        Assert.Equal($"{CSharpMethodsUrl}#dothing-string", item.Url);
        Assert.Contains("## DoThing(string) {#dothing-string}", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_RebasesConfiguredHostForFetchedContentAndReturnedUrl()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [ApiDocsSourceConfiguration.SitemapUrlConfigPath] = "http://localhost:4321/sitemap-0.xml"
            })
            .Build();
 
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/testtype/methods/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["http://localhost:4321/reference/api/csharp/aspire.test.package/testtype/methods.md"] = """
                    # Methods
 
                    - [Package](/reference/api/csharp/aspire.test.package.md)
                    - [CreateBuilder(string[])](/reference/api/csharp/aspire.hosting/distributedapplication/methods.md#createbuilder-string)
                    - [Current](#localonly)
 
                    ## LocalOnly() {#localonly}
                    """
            });
 
        var service = new ApiDocsIndexService(fetcher, new TestApiDocsCache(), configuration, NullLogger<ApiDocsIndexService>.Instance);
 
        var item = await service.GetAsync("csharp/aspire.test.package/testtype/methods");
 
        Assert.NotNull(item);
        Assert.Equal("http://localhost:4321/reference/api/csharp/aspire.test.package/testtype/methods", item.Url);
        Assert.Equal(["http://localhost:4321/reference/api/csharp/aspire.test.package/testtype/methods"], fetcher.RequestedPageUrls);
        Assert.Equal(["http://localhost:4321/reference/api/csharp/aspire.test.package/testtype/methods.md"], fetcher.RequestedMarkdownUrls);
        Assert.Contains("[Package](http://localhost:4321/reference/api/csharp/aspire.test.package.md)", item.Content, StringComparison.Ordinal);
        Assert.Contains("[CreateBuilder(string[])](http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication/methods.md#createbuilder-string)", item.Content, StringComparison.Ordinal);
        Assert.Contains("[Current](http://localhost:4321/reference/api/csharp/aspire.test.package/testtype/methods#localonly)", item.Content, StringComparison.Ordinal);
        Assert.Contains("## LocalOnly()", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_RewritesBracketedMemberSignatureLinksFromDistributedApplicationPage()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [ApiDocsSourceConfiguration.SitemapUrlConfigPath] = "http://localhost:4321/sitemap-0.xml"
            })
            .Build();
 
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplication/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication.md"] = """
                    # DistributedApplication
 
                    ## Methods
 
                    - [CreateBuilder(string[])](/reference/api/csharp/aspire.hosting/distributedapplication/methods.md#createbuilder-string) : [IDistributedApplicationBuilder](/reference/api/csharp/aspire.hosting/idistributedapplicationbuilder.md) `static` `ats export` -- Creates a new instance of [IDistributedApplicationBuilder](/reference/api/csharp/aspire.hosting/idistributedapplicationbuilder.md) with the specified command-line arguments.
                    """
            });
 
        var service = new ApiDocsIndexService(fetcher, new TestApiDocsCache(), configuration, NullLogger<ApiDocsIndexService>.Instance);
 
        var item = await service.GetAsync("csharp/aspire.hosting/distributedapplication/");
 
        Assert.NotNull(item);
        Assert.Equal("http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication", item.Url);
        Assert.Equal(["http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication"], fetcher.RequestedPageUrls);
        Assert.Equal(["http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication.md"], fetcher.RequestedMarkdownUrls);
        Assert.Contains("[CreateBuilder(string[])](http://localhost:4321/reference/api/csharp/aspire.hosting/distributedapplication/methods.md#createbuilder-string)", item.Content, StringComparison.Ordinal);
        Assert.Contains("[IDistributedApplicationBuilder](http://localhost:4321/reference/api/csharp/aspire.hosting/idistributedapplicationbuilder.md)", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_ForTypeScriptDirectRouteItem_ReturnsMarkdownFromConfiguredHost()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [ApiDocsSourceConfiguration.SitemapUrlConfigPath] = "http://localhost:4321/sitemap-0.xml"
            })
            .Build();
 
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.postgresql/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.postgresql/addpostgres/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["http://localhost:4321/reference/api/typescript/aspire.hosting.postgresql/addpostgres.md"] = "# addPostgres\n\n## Parameters\n\n- name"
            });
 
        var service = new ApiDocsIndexService(fetcher, new TestApiDocsCache(), configuration, NullLogger<ApiDocsIndexService>.Instance);
 
        var item = await service.GetAsync("typescript/aspire.hosting.postgresql/addpostgres");
 
        Assert.NotNull(item);
        Assert.Equal(ApiReferenceLanguages.TypeScript, item.Language);
        Assert.Equal(ApiReferenceKinds.Member, item.Kind);
        Assert.Equal("http://localhost:4321/reference/api/typescript/aspire.hosting.postgresql/addpostgres", item.Url);
        Assert.Equal(["http://localhost:4321/reference/api/typescript/aspire.hosting.postgresql/addpostgres"], fetcher.RequestedPageUrls);
        Assert.Equal(["http://localhost:4321/reference/api/typescript/aspire.hosting.postgresql/addpostgres.md"], fetcher.RequestedMarkdownUrls);
        Assert.Contains("# addPostgres", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_ForGoItemPage_ReturnsMarkdownFromConfiguredHost()
    {
        var (service, fetcher) = CreateGoContentService();
 
        var item = await service.GetAsync("go/aspire.hosting/containerresource");
 
        Assert.NotNull(item);
        Assert.Equal(ApiReferenceLanguages.Go, item.Language);
        Assert.Equal(ApiReferenceKinds.Symbol, item.Kind);
        Assert.Equal("go/aspire.hosting", item.ParentId);
        Assert.Equal("http://localhost:4321/reference/api/go/aspire.hosting/containerresource", item.Url);
        Assert.Equal(["http://localhost:4321/reference/api/go/aspire.hosting/containerresource"], fetcher.RequestedPageUrls);
        Assert.Equal(["http://localhost:4321/reference/api/go/aspire.hosting/containerresource.md"], fetcher.RequestedMarkdownUrls);
        Assert.Contains("```go", item.Content, StringComparison.Ordinal);
        Assert.Contains("Args() *List[string]", item.Content, StringComparison.Ordinal);
        Assert.Contains("## Errors", item.Content, StringComparison.Ordinal);
        Assert.Contains("[WithEndpoint](http://localhost:4321/reference/api/go/aspire.hosting/containerresource/withendpoint.md)", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task GetAsync_ForGoMemberPage_ReturnsMarkdownFromConfiguredHost()
    {
        var (service, fetcher) = CreateGoContentService();
 
        var item = await service.GetAsync("go/aspire.hosting/containerresource/withendpoint");
 
        Assert.NotNull(item);
        Assert.Equal(ApiReferenceLanguages.Go, item.Language);
        Assert.Equal(ApiReferenceKinds.Member, item.Kind);
        Assert.Equal("go/aspire.hosting/containerresource", item.ParentId);
        Assert.Equal("http://localhost:4321/reference/api/go/aspire.hosting/containerresource/withendpoint", item.Url);
        Assert.Equal(["http://localhost:4321/reference/api/go/aspire.hosting/containerresource/withendpoint"], fetcher.RequestedPageUrls);
        Assert.Equal(["http://localhost:4321/reference/api/go/aspire.hosting/containerresource/withendpoint.md"], fetcher.RequestedMarkdownUrls);
        Assert.Contains("## Options", item.Content, StringComparison.Ordinal);
        Assert.Contains("`Tags` (`[]string`)", item.Content, StringComparison.Ordinal);
        Assert.Contains("`Metadata` (`map[string]string`)", item.Content, StringComparison.Ordinal);
        Assert.Contains("check `Err()` for the `error`", item.Content, StringComparison.Ordinal);
        Assert.Contains("[ContainerResource](http://localhost:4321/reference/api/go/aspire.hosting/containerresource.md)", item.Content, StringComparison.Ordinal);
    }
 
    [Fact]
    public async Task EnsureIndexedAsync_RebuildsCachedIndexWhenSitemapChangesAcrossInstances()
    {
        var cache = new TestApiDocsCache();
        var fetcher = new SequenceApiDocsFetcher(
        [
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.first.package/</loc></url>
            </urlset>
            """,
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.second.package/</loc></url>
            </urlset>
            """
        ]);
 
        var firstService = new ApiDocsIndexService(fetcher, cache, new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
        var firstItems = await firstService.ListAsync("csharp");
 
        Assert.Collection(
            firstItems,
            item => Assert.Equal("csharp/aspire.first.package", item.Id));
 
        var secondService = new ApiDocsIndexService(fetcher, cache, new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
        var secondItems = await secondService.ListAsync("csharp");
 
        Assert.Collection(
            secondItems,
            item => Assert.Equal("csharp/aspire.second.package", item.Id));
    }
 
    [Fact]
    public async Task EnsureIndexedAsync_UsesCachedIndexWhenSitemapUnavailableAfterInitialLoad()
    {
        var cache = new TestApiDocsCache();
        var firstService = new ApiDocsIndexService(
            new SequenceApiDocsFetcher(
            [
                """
                <?xml version="1.0" encoding="UTF-8"?>
                <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
                  <url><loc>https://aspire.dev/reference/api/csharp/aspire.cached.package/</loc></url>
                </urlset>
                """
            ]),
            cache,
            new ConfigurationBuilder().Build(),
            NullLogger<ApiDocsIndexService>.Instance);
 
        await firstService.EnsureIndexedAsync();
 
        var secondService = new ApiDocsIndexService(
            new SequenceApiDocsFetcher([null]),
            cache,
            new ConfigurationBuilder().Build(),
            NullLogger<ApiDocsIndexService>.Instance);
 
        var items = await secondService.ListAsync("csharp");
 
        Assert.Collection(
            items,
            item => Assert.Equal("csharp/aspire.cached.package", item.Id));
    }
 
    [Fact]
    public async Task EnsureIndexedAsync_RebuildsIndexCachedByPreviousSchemaVersion()
    {
        var sitemap = """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/</loc></url>
            </urlset>
            """;
 
        var cache = new TestApiDocsCache();
        await cache.SetIndexAsync(
        [
            new ApiReferenceItem
            {
                Id = "csharp/aspire.test.package",
                Name = "aspire.test.package",
                Language = ApiReferenceLanguages.CSharp,
                Kind = ApiReferenceKinds.Package,
                PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package",
                ParentId = ApiReferenceLanguages.CSharp
            }
        ]);
 
        // Schema version 1 predates Go routes, so an index cached under it was built without them.
        var previousFingerprint = SourceContentFingerprint.Compute(sitemap, schemaVersion: 1);
        await cache.SetIndexSourceFingerprintAsync(previousFingerprint);
 
        var service = new ApiDocsIndexService(
            new SequenceApiDocsFetcher([sitemap]),
            cache,
            new ConfigurationBuilder().Build(),
            NullLogger<ApiDocsIndexService>.Instance);
 
        var goItems = await service.ListAsync("go");
 
        var module = Assert.Single(goItems);
        Assert.Equal("go/aspire.hosting", module.Id);
        Assert.NotEqual(previousFingerprint, await cache.GetIndexSourceFingerprintAsync());
    }
 
    private static ApiDocsIndexService CreateService(IConfiguration? configuration = null)
    {
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/testtype/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/testtype/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.test/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.test/testresource/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.test/testresource/runasemulator/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                [CSharpMethodsUrl] = """
                    # Methods
                    
                    ## DoThing(string) {#dothing-string}
 
                    - Name: `DoThing(string)`
                    - Returns: `void`
 
                    Does the thing.
 
                    ## Parameters
 
                    - `value` (`string`)
                      The value to process.
 
                    ## DoOther() {#doother}
 
                    - Name: `DoOther()`
                    - Returns: `void`
 
                    Does something else.
                    """,
                [CSharpTypeUrl] = """
                    # TestType
 
                    Provides a test type.
 
                    ## Methods
 
                    - [DoThing(string)](/reference/api/csharp/aspire.test.package/testtype/methods.md#dothing-string) : `void` `extension` -- Does the thing.
                    - [DoOther()](/reference/api/csharp/aspire.test.package/testtype/methods.md#doother) : `void` `extension` -- Does something else.
                    """,
            });
 
        return new ApiDocsIndexService(fetcher, new TestApiDocsCache(), configuration ?? new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
    }
 
    private static ApiDocsIndexService CreateWithEnvironmentService()
    {
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/iresourcewithenvironment/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["https://aspire.dev/reference/api/csharp/aspire.test.package/iresourcewithenvironment"] = """
                    # IResourceWithEnvironment
 
                    Provides access to environment configuration.
                    """,
                [CSharpWithEnvironmentTypeUrl] = """
                    # ResourceBuilderExtensions
 
                    Provides extension methods for configuring resources with environment variables.
 
                    ## Methods
 
                    - [WithEnvironment(IResourceBuilder<T>, string, string?)](/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods.md#withenvironment-iresourcebuilder-t-string-string) : `IResourceBuilder<T>` `extension` -- Adds a string environment variable.
                    - [WithEnvironment(IResourceBuilder<T>, Action<EnvironmentCallbackContext>)](/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods.md#withenvironment-iresourcebuilder-t-action-environmentcallbackcontext) : `IResourceBuilder<T>` `extension` -- Adds environment variables from a callback.
                    """,
                [CSharpWithEnvironmentMethodsUrl] = """
                    # Methods
 
                    ## WithEnvironment(IResourceBuilder<T>, string, string?) {#withenvironment-iresourcebuilder-t-string-string}
 
                    Adds a string environment variable.
 
                    ## WithEnvironment(IResourceBuilder<T>, Action<EnvironmentCallbackContext>) {#withenvironment-iresourcebuilder-t-action-environmentcallbackcontext}
 
                    Adds environment variables from a callback.
                    """
            });
 
        return new ApiDocsIndexService(fetcher, new TestApiDocsCache(), new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
    }
 
    private static ApiDocsIndexService CreateDistributedApplicationService()
    {
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplication/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplication/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplicationbuilder/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplicationbuilder/constructors/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/idistributedapplicationeventingsubscriber/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting/idistributedapplicationeventingsubscriber/methods/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting.testing/</loc></url>
              <url><loc>https://aspire.dev/reference/api/csharp/aspire.hosting.testing/distributedapplicationfactory/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplication"] = """
                    # DistributedApplication
 
                    Represents a distributed application.
 
                    ## Methods
 
                    - [CreateBuilder(DistributedApplicationOptions)](/reference/api/csharp/aspire.hosting/distributedapplication/methods.md#createbuilder-distributedapplicationoptions) : `DistributedApplicationBuilder` -- Creates a builder.
                    """,
                ["https://aspire.dev/reference/api/csharp/aspire.hosting/distributedapplicationbuilder"] = """
                    # DistributedApplicationBuilder
 
                    Builds a distributed application.
 
                    ## Constructors
 
                    - [DistributedApplicationBuilder(DistributedApplicationOptions)](/reference/api/csharp/aspire.hosting/distributedapplicationbuilder/constructors.md#constructor-distributedapplicationoptions) : `DistributedApplicationBuilder` -- Creates a builder instance.
                    """,
                ["https://aspire.dev/reference/api/csharp/aspire.hosting/idistributedapplicationeventingsubscriber"] = """
                    # IDistributedApplicationEventingSubscriber
 
                    Subscribes to distributed application events.
 
                    ## Methods
 
                    - [SubscribeAsync(IDistributedApplicationEventing, DistributedApplicationExecutionContext, CancellationToken)](/reference/api/csharp/aspire.hosting/idistributedapplicationeventingsubscriber/methods.md#subscribeasync-idistributedapplicationeventing-distributedapplicationexecutioncontext-cancellationtoken) : `Task` -- Subscribes to events.
                    """,
                ["https://aspire.dev/reference/api/csharp/aspire.hosting.testing/distributedapplicationfactory"] = """
                    # DistributedApplicationFactory
 
                    Creates distributed application test hosts.
                    """
            });
 
        return new ApiDocsIndexService(fetcher, new TestApiDocsCache(), new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
    }
 
    private static ApiDocsIndexService CreateGoAndTypeScriptService()
    {
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/go/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/containerresource/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/containerresource/withendpoint/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/containerresource/withenvironment/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/environmentcallbackcontext/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting.redis/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting.redis/addredis/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting/containerresource/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.redis/</loc></url>
              <url><loc>https://aspire.dev/reference/api/typescript/aspire.hosting.redis/addredis/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>());
 
        return new ApiDocsIndexService(fetcher, new TestApiDocsCache(), new ConfigurationBuilder().Build(), NullLogger<ApiDocsIndexService>.Instance);
    }
 
    private static (ApiDocsIndexService Service, TestApiDocsFetcher Fetcher) CreateGoContentService()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                [ApiDocsSourceConfiguration.SitemapUrlConfigPath] = "http://localhost:4321/sitemap-0.xml"
            })
            .Build();
 
        var fetcher = new TestApiDocsFetcher(
            """
            <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/containerresource/</loc></url>
              <url><loc>https://aspire.dev/reference/api/go/aspire.hosting/containerresource/withendpoint/</loc></url>
            </urlset>
            """,
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["http://localhost:4321/reference/api/go/aspire.hosting/containerresource.md"] = """
                    # ContainerResource
 
                    `interface` -- Represents a container resource.
 
                    ## Definition
 
                    ```go
                    type ContainerResource interface {
                        handleReference
                        Args() *List[string]
                        Err() error
                        WithEndpoint(options ...*WithEndpointOptions) ContainerResource
                        WithEnvironment(name string, value any) ContainerResource
                    }
                    ```
 
                    ## Methods
 
                    - [WithEndpoint](/reference/api/go/aspire.hosting/containerresource/withendpoint.md) -- Adds a network endpoint.
                    - [WithEnvironment](/reference/api/go/aspire.hosting/containerresource/withenvironment.md) -- Sets an environment variable.
 
                    ## Errors
 
                    Methods that return a handle record a failure and report it through `Err()`.
 
                    ## Applies to
 
                    - [aspire.hosting](/reference/api/go/aspire.hosting.md)
                    """,
                ["http://localhost:4321/reference/api/go/aspire.hosting/containerresource/withendpoint.md"] = """
                    # WithEndpoint
 
                    ## Definition
 
                    ```go
                    WithEndpoint(options ...*WithEndpointOptions) ContainerResource
                    ```
 
                    ## Signature
 
                    ```go
                    WithEndpoint(options ...*WithEndpointOptions) ContainerResource
                    ```
 
                    ## Parameters
 
                    - `options` (`...*WithEndpointOptions`) -- Optional settings for the endpoint.
 
                    ## Options
 
                    - `Port` (`*float64`) -- The port to bind.
                    - `Name` (`*string`) -- The endpoint name.
                    - `Tags` (`[]string`) -- Labels applied to the endpoint.
                    - `Metadata` (`map[string]string`) -- Additional endpoint metadata.
 
                    ## Returns
 
                    [ContainerResource](/reference/api/go/aspire.hosting/containerresource.md) -- The same handle, so calls can be chained. Failures are deferred: check `Err()` for the `error`.
 
                    ## Defined on
 
                    - [ContainerResource](/reference/api/go/aspire.hosting/containerresource.md)
                    """
            });
 
        var service = new ApiDocsIndexService(fetcher, new TestApiDocsCache(), configuration, NullLogger<ApiDocsIndexService>.Instance);
 
        return (service, fetcher);
    }
 
    private sealed class TestApiDocsFetcher(string sitemapContent, IReadOnlyDictionary<string, string> pageContent) : IApiDocsFetcher
    {
        private readonly object _lock = new();
 
        public List<string> RequestedPageUrls { get; } = [];
 
        public List<string> RequestedMarkdownUrls { get; } = [];
 
        public Task<string?> FetchSitemapAsync(CancellationToken cancellationToken = default)
            => Task.FromResult<string?>(sitemapContent);
 
        public Task<string?> FetchPageAsync(string pageUrl, CancellationToken cancellationToken = default)
        {
            var cachePageUrl = pageUrl.Split('#', 2)[0];
            var markdownUrl = cachePageUrl.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? cachePageUrl : $"{cachePageUrl}.md";
 
            lock (_lock)
            {
                RequestedPageUrls.Add(pageUrl);
                RequestedMarkdownUrls.Add(markdownUrl);
            }
 
            return Task.FromResult(pageContent.TryGetValue(cachePageUrl, out var content) ? content : pageContent.TryGetValue(markdownUrl, out content) ? content : null);
        }
    }
 
    private sealed class SequenceApiDocsFetcher(IEnumerable<string?> sitemapContents) : IApiDocsFetcher
    {
        private readonly Queue<string?> _sitemapContents = new(sitemapContents);
 
        public Task<string?> FetchSitemapAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(_sitemapContents.Count > 0 ? _sitemapContents.Dequeue() : null);
 
        public Task<string?> FetchPageAsync(string pageUrl, CancellationToken cancellationToken = default)
            => Task.FromResult<string?>(null);
    }
 
    private sealed class TestApiDocsCache : IApiDocsCache
    {
        private readonly Dictionary<string, string> _content = new(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, string?> _etags = new(StringComparer.OrdinalIgnoreCase);
        private string? _indexSourceFingerprint;
        private string? _memberIndexSourceFingerprint;
        private string[]? _indexedMemberContainerIds;
 
        public ApiReferenceItem[]? Index { get; private set; }
        public ApiReferenceItem[]? MemberIndex { get; private set; }
 
        public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default)
            => Task.FromResult(_content.TryGetValue(key, out var value) ? value : null);
 
        public Task SetAsync(string key, string content, CancellationToken cancellationToken = default)
        {
            _content[key] = content;
            return Task.CompletedTask;
        }
 
        public Task<string?> GetETagAsync(string url, CancellationToken cancellationToken = default)
            => Task.FromResult(_etags.TryGetValue(url, out var value) ? value : null);
 
        public Task SetETagAsync(string url, string? etag, CancellationToken cancellationToken = default)
        {
            if (etag is null)
            {
                _etags.Remove(url);
            }
            else
            {
                _etags[url] = etag;
            }
 
            return Task.CompletedTask;
        }
 
        public Task InvalidateAsync(string key, CancellationToken cancellationToken = default)
        {
            _content.Remove(key);
            return Task.CompletedTask;
        }
 
        public Task<ApiReferenceItem[]?> GetIndexAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(Index);
 
        public Task SetIndexAsync(ApiReferenceItem[] documents, CancellationToken cancellationToken = default)
        {
            Index = documents;
            return Task.CompletedTask;
        }
 
        public Task<string?> GetIndexSourceFingerprintAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(_indexSourceFingerprint);
 
        public Task SetIndexSourceFingerprintAsync(string fingerprint, CancellationToken cancellationToken = default)
        {
            _indexSourceFingerprint = fingerprint;
            return Task.CompletedTask;
        }
 
        public Task<ApiReferenceItem[]?> GetMemberIndexAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(MemberIndex);
 
        public Task SetMemberIndexAsync(ApiReferenceItem[] documents, CancellationToken cancellationToken = default)
        {
            MemberIndex = documents;
            return Task.CompletedTask;
        }
 
        public Task<string?> GetMemberIndexSourceFingerprintAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(_memberIndexSourceFingerprint);
 
        public Task SetMemberIndexSourceFingerprintAsync(string fingerprint, CancellationToken cancellationToken = default)
        {
            _memberIndexSourceFingerprint = fingerprint;
            return Task.CompletedTask;
        }
 
        public Task<string[]?> GetIndexedMemberContainerIdsAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(_indexedMemberContainerIds);
 
        public Task SetIndexedMemberContainerIdsAsync(string[] containerIds, CancellationToken cancellationToken = default)
        {
            _indexedMemberContainerIds = containerIds;
            return Task.CompletedTask;
        }
    }
}