From 5c69287c44a7634c6757660186dbfa745d08ba2e Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 16 Apr 2026 21:32:32 +0200 Subject: [PATCH] Extend the DocApiGen and DocViewer projects --- .../ServiceCollectionExtensions.cs | 5 +- .../FluentUIComponentsIntegrationTests.cs | 110 ++++++++++++------ .../Models/IconEmoji/IconEmojiTests.cs | 29 ++--- .../SummaryMode/ApiClassPerformanceTests.cs | 28 ++--- .../IDocumentationCommentProvider.cs | 27 +++++ .../DocumentationAssemblyLoadContext.cs | 83 +++++++++++++ .../Generators/AllDocumentationGenerator.cs | 25 ++-- .../Generators/DocumentationGeneratorBase.cs | 28 +++-- .../DocumentationGeneratorFactory.cs | 39 +++++-- .../Generators/IconsEmojisGenerator.cs | 9 +- .../Generators/McpDocumentationGenerator.cs | 26 +++-- .../SummaryDocumentationGenerator.cs | 37 ++++-- .../Models/DocumentationCommentProvider.cs | 67 +++++++++++ .../Models/DocumentationInput.cs | 46 ++++++++ .../Models/SummaryMode/ApiClass.cs | 4 +- .../Models/SummaryMode/ApiClassOptions.cs | 10 +- .../Tools/FluentUI.Demo.DocApiGen/Program.cs | 90 +++++++++++--- .../Components/MarkdownViewer.razor.cs | 7 +- .../Services/DocViewerOptions.cs | 12 +- .../Services/DocViewerService.cs | 52 ++++++++- 20 files changed, 579 insertions(+), 155 deletions(-) create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationCommentProvider.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/DocumentationAssemblyLoadContext.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationCommentProvider.cs create mode 100644 examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationInput.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs index 2a525279fe..1cb3188b35 100644 --- a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs +++ b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs @@ -64,7 +64,10 @@ public static DemoServices AddFluentUIDemoServices(this IServiceCollection servi options.PageTitle = "{0} - FluentUI Blazor Components"; options.ComponentsAssembly = typeof(Client._Imports).Assembly; options.ResourcesAssembly = typeof(Client._Imports).Assembly; - options.ApiAssembly = typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly; + options.ApiAssemblies = + [ + typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly, + ]; options.ApiCommentSummary = (data, component, member) => { if (member is null && (data is null || data?.Items?.Count <= 1)) diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs index 352a5bab37..72a6a6d3da 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen.IntegrationTests/FluentUIComponentsIntegrationTests.cs @@ -2,13 +2,12 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using FluentUI.Demo.DocApiGen; +using System.Reflection; +using System.Text.Json; using FluentUI.Demo.DocApiGen.Abstractions; using FluentUI.Demo.DocApiGen.Formatters; using FluentUI.Demo.DocApiGen.Generators; -using System.Reflection; -using System.Text.Json; -using Xunit; +using FluentUI.Demo.DocApiGen.Models; namespace FluentUI.Demo.DocApiGen.IntegrationTests; @@ -24,7 +23,9 @@ public class FluentUIComponentsIntegrationTests : IDisposable private readonly FileInfo _xmlDocumentation; private readonly string _tempOutputDirectory; private readonly string _xmlPath; + private readonly DocumentationAssemblyLoadContext _assemblyLoadContext; private readonly Assembly _fluentUIAssembly; + private readonly IReadOnlyList _documentationInputs; /// /// Initializes a new instance of the class. @@ -44,22 +45,30 @@ public FluentUIComponentsIntegrationTests() } _xmlDocumentation = new FileInfo(_xmlPath); + _assemblyLoadContext = new DocumentationAssemblyLoadContext([GetAssemblyPath(projectRoot, Path.Combine("src", "Core"), "Microsoft.FluentUI.AspNetCore.Components.dll")]); // Load the FluentUI assembly dynamically - var fluentUIAssemblyPath = Path.Combine(projectRoot, "src", "Core", "bin", "Debug", NET_VERSION, "Microsoft.FluentUI.AspNetCore.Components.dll"); + var fluentUIAssemblyPath = GetAssemblyPath(projectRoot, Path.Combine("src", "Core"), "Microsoft.FluentUI.AspNetCore.Components.dll"); - if (!File.Exists(fluentUIAssemblyPath)) + _fluentUIAssembly = _assemblyLoadContext.LoadFromAssemblyPath(fluentUIAssemblyPath); + _documentationInputs = [new DocumentationInput(_fluentUIAssembly, _xmlDocumentation)]; + } + + private static string GetAssemblyPath(string projectRoot, string relativeProjectDirectory, string assemblyFileName) + { + var debugPath = Path.Combine(projectRoot, relativeProjectDirectory, "bin", "Debug", NET_VERSION, assemblyFileName); + if (File.Exists(debugPath)) { - // Try alternative path (Release build) - fluentUIAssemblyPath = Path.Combine(projectRoot, "src", "Core", "bin", "Release", NET_VERSION, "Microsoft.FluentUI.AspNetCore.Components.dll"); + return debugPath; + } - if (!File.Exists(fluentUIAssemblyPath)) - { - throw new FileNotFoundException($"FluentUI assembly not found. Please build the Core project first. Looked for: {fluentUIAssemblyPath}"); - } + var releasePath = Path.Combine(projectRoot, relativeProjectDirectory, "bin", "Release", NET_VERSION, assemblyFileName); + if (File.Exists(releasePath)) + { + return releasePath; } - _fluentUIAssembly = Assembly.LoadFrom(fluentUIAssemblyPath); + throw new FileNotFoundException($"Assembly not found. Please build the project first. Looked for: {releasePath}"); } /// @@ -72,7 +81,7 @@ private static string GetProjectRootDirectory() // Look for solution file while (directory != null) { - var solutionFiles = directory.GetFiles("*.sln"); + var solutionFiles = directory.GetFiles("*.slnx"); if (solutionFiles.Length > 0) { return directory.FullName; @@ -108,7 +117,7 @@ public void Dispose() public void SummaryGenerator_ShouldGenerateJsonSuccessfully() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -125,7 +134,7 @@ public void SummaryGenerator_ShouldGenerateJsonSuccessfully() public void SummaryGenerator_ShouldGenerateCSharpSuccessfully() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateCSharpFormatter(); // Act @@ -143,7 +152,7 @@ public void SummaryGenerator_ShouldGenerateCSharpSuccessfully() public void SummaryGenerator_JsonOutput_ShouldContainFluentUIComponents() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -158,7 +167,7 @@ public void SummaryGenerator_JsonOutput_ShouldContainFluentUIComponents() public void SummaryGenerator_CSharpOutput_ShouldContainFluentUIComponents() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateCSharpFormatter(); // Act @@ -173,7 +182,7 @@ public void SummaryGenerator_CSharpOutput_ShouldContainFluentUIComponents() public void SummaryGenerator_JsonOutput_ShouldBeValidJson() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -188,7 +197,7 @@ public void SummaryGenerator_JsonOutput_ShouldBeValidJson() public void SummaryGenerator_SaveToFile_JsonShouldSucceed() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_summary.json"); @@ -211,7 +220,7 @@ public void SummaryGenerator_SaveToFile_JsonShouldSucceed() public void SummaryGenerator_SaveToFile_CSharpShouldSucceed() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateCSharpFormatter(); var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_summary.cs"); @@ -230,7 +239,7 @@ public void SummaryGenerator_SaveToFile_CSharpShouldSucceed() public void SummaryGenerator_LargeScale_ShouldCompleteWithoutErrors() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var jsonFormatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); var csharpFormatter = OutputFormatterFactory.CreateCSharpFormatter(); @@ -253,7 +262,7 @@ public void SummaryGenerator_LargeScale_ShouldCompleteWithoutErrors() public void SummaryGenerator_OutputSize_ShouldBeReasonable() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var jsonFormatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); var csharpFormatter = OutputFormatterFactory.CreateCSharpFormatter(); @@ -273,7 +282,7 @@ public void SummaryGenerator_OutputSize_ShouldBeReasonable() public void SummaryGenerator_JsonMetadata_ShouldBePresent() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -296,7 +305,7 @@ public void SummaryGenerator_JsonMetadata_ShouldBePresent() public void SummaryGenerator_CompactFormat_ShouldGenerateCorrectStructure() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -315,7 +324,7 @@ public void SummaryGenerator_CompactFormat_ShouldGenerateCorrectStructure() public void SummaryGenerator_CompactFormat_ShouldBeValidJson() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); // Act @@ -330,7 +339,7 @@ public void SummaryGenerator_CompactFormat_ShouldBeValidJson() public void SummaryGenerator_CompactFormat_SaveToFile_ShouldSucceed() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: true); var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_compact.json"); @@ -349,6 +358,41 @@ public void SummaryGenerator_CompactFormat_SaveToFile_ShouldSucceed() Assert.Null(exception); } + [Fact] + public void AllGenerator_WithMultipleDocumentationInputs_ShouldIncludeChartsAssemblyTypes() + { + // Arrange + var projectRoot = GetProjectRootDirectory(); + var chartsAssemblyPath = Path.Combine(projectRoot, "src", "Charts", "bin", "Debug", NET_VERSION, "Microsoft.FluentUI.AspNetCore.Components.Charts.dll"); + + if (!File.Exists(chartsAssemblyPath)) + { + chartsAssemblyPath = Path.Combine(projectRoot, "src", "Charts", "bin", "Release", NET_VERSION, "Microsoft.FluentUI.AspNetCore.Components.Charts.dll"); + } + + var chartsXmlPath = Path.Combine(projectRoot, "examples", "Tools", "FluentUI.Demo.DocApiGen", "Microsoft.FluentUI.AspNetCore.Components.Charts.xml"); + + if (!File.Exists(chartsAssemblyPath) || !File.Exists(chartsXmlPath)) + { + return; + } + + var inputs = new List(_documentationInputs) + { + new(new DocumentationAssemblyLoadContext([chartsAssemblyPath]).LoadFromAssemblyPath(chartsAssemblyPath), new FileInfo(chartsXmlPath)) + }; + + var generator = DocumentationGeneratorFactory.Create(GenerationMode.All, inputs); + var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); + + // Act + var json = generator.Generate(formatter); + + // Assert + Assert.Contains("FluentDonutChart", json); + Assert.Contains("FluentHorizontalBarChart", json); + } + #endregion #region Summary Mode Tests - Structured Format (Extended) @@ -357,7 +401,7 @@ public void SummaryGenerator_CompactFormat_SaveToFile_ShouldSucceed() public void SummaryGenerator_StructuredFormat_ShouldContainMetadata() { // Arrange - var generator = DocumentationGeneratorFactory.CreateSummaryGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.Summary, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); // Act @@ -381,7 +425,7 @@ public void SummaryGenerator_StructuredFormat_ShouldContainMetadata() public void AllGenerator_ShouldGenerateJsonSuccessfully() { // Arrange - var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.All, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); // Act @@ -398,7 +442,7 @@ public void AllGenerator_ShouldGenerateJsonSuccessfully() public void AllGenerator_ShouldNotSupportCSharpFormat() { // Arrange - var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.All, _documentationInputs); var formatter = OutputFormatterFactory.CreateCSharpFormatter(); // Act & Assert @@ -410,7 +454,7 @@ public void AllGenerator_ShouldNotSupportCSharpFormat() public void AllGenerator_JsonOutput_ShouldContainComponents() { // Arrange - var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.All, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); // Act @@ -427,7 +471,7 @@ public void AllGenerator_JsonOutput_ShouldContainComponents() public void AllGenerator_SaveToFile_ShouldSucceed() { // Arrange - var generator = DocumentationGeneratorFactory.CreateAllGenerator(_fluentUIAssembly, _xmlDocumentation); + var generator = DocumentationGeneratorFactory.Create(GenerationMode.All, _documentationInputs); var formatter = OutputFormatterFactory.CreateJsonFormatter(useCompactFormat: false); var outputPath = Path.Combine(_tempOutputDirectory, "fluentui_all.json"); @@ -478,7 +522,7 @@ private static (Assembly McpAssembly, FileInfo McpXml)? TryLoadMcpAssembly() return null; } - var mcpAssembly = Assembly.LoadFrom(mcpAssemblyPath); + var mcpAssembly = new DocumentationAssemblyLoadContext([mcpAssemblyPath]).LoadFromAssemblyPath(mcpAssemblyPath); var mcpXml = new FileInfo(mcpXmlPath); return (mcpAssembly, mcpXml); diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/IconEmoji/IconEmojiTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/IconEmoji/IconEmojiTests.cs index f27c0e9ca0..7360335f04 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/IconEmoji/IconEmojiTests.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/IconEmoji/IconEmojiTests.cs @@ -7,6 +7,7 @@ using FluentUI.Demo.DocApiGen.Abstractions; using FluentUI.Demo.DocApiGen.Formatters; using FluentUI.Demo.DocApiGen.Generators; +using FluentUI.Demo.DocApiGen.Models; using Xunit; namespace FluentUI.Demo.DocApiGen.Tests.Models.IconEmoji; @@ -18,6 +19,7 @@ public class IconEmojiTests { private readonly Assembly _testAssembly; private readonly FileInfo _testXmlFile; + private readonly IReadOnlyList _testInputs; public IconEmojiTests() { @@ -29,13 +31,14 @@ public IconEmojiTests() var tempPath = Path.Combine(Path.GetTempPath(), "test.xml"); File.WriteAllText(tempPath, ""); _testXmlFile = new FileInfo(tempPath); + _testInputs = [new DocumentationInput(_testAssembly, _testXmlFile)]; } [Fact] public void IconsMode_Constructor_ShouldSetCorrectMode() { // Arrange & Act - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); // Assert Assert.Equal(GenerationMode.Icons, generator.Mode); @@ -45,7 +48,7 @@ public void IconsMode_Constructor_ShouldSetCorrectMode() public void EmojisMode_Constructor_ShouldSetCorrectMode() { // Arrange & Act - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); // Assert Assert.Equal(GenerationMode.Emojis, generator.Mode); @@ -77,7 +80,7 @@ public void Factory_CreateEmojisGenerator_ShouldReturnIconsEmojisGenerator() public void IconsMode_Generate_ShouldProduceValidJson() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); var formatter = new JsonOutputFormatter(); // Act @@ -96,7 +99,7 @@ public void IconsMode_Generate_ShouldProduceValidJson() public void EmojisMode_Generate_ShouldProduceValidJson() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); var formatter = new JsonOutputFormatter(); // Act @@ -115,7 +118,7 @@ public void EmojisMode_Generate_ShouldProduceValidJson() public void IconsMode_Generate_ShouldContainExpectedStructure() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); var formatter = new JsonOutputFormatter(); // Act @@ -152,7 +155,7 @@ public void IconsMode_Generate_ShouldContainExpectedStructure() public void EmojisMode_Generate_ShouldContainExpectedStructure() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); var formatter = new JsonOutputFormatter(); // Act @@ -190,7 +193,7 @@ public void EmojisMode_Generate_ShouldContainExpectedStructure() public void IconsMode_Generate_ShouldExcludeCustomSize() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); var formatter = new JsonOutputFormatter(); // Act @@ -214,7 +217,7 @@ public void IconsMode_Generate_ShouldExcludeCustomSize() public void EmojisMode_Generate_ShouldExcludeCustomSize() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); var formatter = new JsonOutputFormatter(); // Act @@ -241,7 +244,7 @@ public void EmojisMode_Generate_ShouldExcludeCustomSize() public void IconsMode_SaveToFile_ShouldCreateFile() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); var formatter = new JsonOutputFormatter(); var outputPath = Path.Combine(Path.GetTempPath(), $"icons-test-{Guid.NewGuid()}.json"); @@ -273,7 +276,7 @@ public void IconsMode_SaveToFile_ShouldCreateFile() public void EmojisMode_SaveToFile_ShouldCreateFile() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); var formatter = new JsonOutputFormatter(); var outputPath = Path.Combine(Path.GetTempPath(), $"emojis-test-{Guid.NewGuid()}.json"); @@ -306,7 +309,7 @@ public void IconsMode_Generate_WithInvalidMode_ShouldThrowNotSupportedException( { // Arrange // Create generator with Summary mode (not supported by IconsEmojisGenerator) - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Summary); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Summary); var formatter = new JsonOutputFormatter(); // Act & Assert @@ -317,7 +320,7 @@ public void IconsMode_Generate_WithInvalidMode_ShouldThrowNotSupportedException( public void IconsMode_Generate_ShouldOrderIconsByName() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Icons); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Icons); var formatter = new JsonOutputFormatter(); // Act @@ -337,7 +340,7 @@ public void IconsMode_Generate_ShouldOrderIconsByName() public void EmojisMode_Generate_ShouldOrderEmojisByName() { // Arrange - var generator = new IconsEmojisGenerator(_testAssembly, _testXmlFile, GenerationMode.Emojis); + var generator = new IconsEmojisGenerator(_testInputs, GenerationMode.Emojis); var formatter = new JsonOutputFormatter(); // Act diff --git a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs index 6fb8f45ccc..ba0b405681 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen.Tests/Models/SummaryMode/ApiClassPerformanceTests.cs @@ -3,6 +3,8 @@ // ------------------------------------------------------------------------ using Xunit; +using System.Reflection; +using FluentUI.Demo.DocApiGen.Models; using FluentUI.Demo.DocApiGen.Models.SummaryMode; using Microsoft.AspNetCore.Components; @@ -21,8 +23,8 @@ public void ApiClass_ShouldHandleAbstractTypes_WithoutExceptions() { // Arrange var assembly = typeof(TestAbstractClass).Assembly; - var docReader = CreateMockDocReader(); - var options = new ApiClassOptions(assembly, docReader); + var commentProvider = CreateMockCommentProvider(assembly); + var options = new ApiClassOptions(assembly, commentProvider); // Act var exception = Record.Exception(() => @@ -43,8 +45,8 @@ public void ApiClass_ShouldHandleInterfaces_WithoutExceptions() { // Arrange var assembly = typeof(ITestInterface).Assembly; - var docReader = CreateMockDocReader(); - var options = new ApiClassOptions(assembly, docReader); + var commentProvider = CreateMockCommentProvider(assembly); + var options = new ApiClassOptions(assembly, commentProvider); // Act var exception = Record.Exception(() => @@ -65,8 +67,8 @@ public void ApiClass_ShouldHandleComplexConstructors_WithoutExceptions() { // Arrange var assembly = typeof(TestClassWithComplexConstructor).Assembly; - var docReader = CreateMockDocReader(); - var options = new ApiClassOptions(assembly, docReader); + var commentProvider = CreateMockCommentProvider(assembly); + var options = new ApiClassOptions(assembly, commentProvider); // Act var exception = Record.Exception(() => @@ -87,8 +89,8 @@ public void ApiClass_ShouldHandleConcreteTypes_Successfully() { // Arrange var assembly = typeof(TestConcreteClass).Assembly; - var docReader = CreateMockDocReader(); - var options = new ApiClassOptions(assembly, docReader); + var commentProvider = CreateMockCommentProvider(assembly); + var options = new ApiClassOptions(assembly, commentProvider); // Act var apiClass = new ApiClass(typeof(TestConcreteClass), options); @@ -108,8 +110,8 @@ public void ApiClass_ShouldProcessMultipleTypes_Efficiently() { // Arrange var assembly = typeof(TestAbstractClass).Assembly; - var docReader = CreateMockDocReader(); - var options = new ApiClassOptions(assembly, docReader); + var commentProvider = CreateMockCommentProvider(assembly); + var options = new ApiClassOptions(assembly, commentProvider); var types = new[] { @@ -143,9 +145,9 @@ public void ApiClass_ShouldProcessMultipleTypes_Efficiently() } /// - /// Creates a mock DocXmlReader for testing. + /// Creates a mock documentation comment provider for testing. /// - private static LoxSmoke.DocXml.DocXmlReader CreateMockDocReader() + private static DocumentationCommentProvider CreateMockCommentProvider(Assembly assembly) { // Create a minimal XML documentation file for testing var xmlContent = @" @@ -160,7 +162,7 @@ private static LoxSmoke.DocXml.DocXmlReader CreateMockDocReader() var tempFile = Path.GetTempFileName(); File.WriteAllText(tempFile, xmlContent); - return new LoxSmoke.DocXml.DocXmlReader(tempFile); + return new DocumentationCommentProvider([new DocumentationInput(assembly, new FileInfo(tempFile))]); } #region Test Types diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationCommentProvider.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationCommentProvider.cs new file mode 100644 index 0000000000..dc01ef1eed --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Abstractions/IDocumentationCommentProvider.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; + +namespace FluentUI.Demo.DocApiGen.Abstractions; + +/// +/// Provides XML documentation comments for reflected types and members. +/// +public interface IDocumentationCommentProvider +{ + /// + /// Gets the summary text for a type. + /// + /// The reflected type. + /// The resolved summary, or an empty string. + string GetComponentSummary(Type type); + + /// + /// Gets the summary text for a member. + /// + /// The reflected member. + /// The resolved summary, or an empty string. + string GetMemberSummary(MemberInfo member); +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/DocumentationAssemblyLoadContext.cs b/examples/Tools/FluentUI.Demo.DocApiGen/DocumentationAssemblyLoadContext.cs new file mode 100644 index 0000000000..27bb8344b1 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/DocumentationAssemblyLoadContext.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Runtime.Loader; + +namespace FluentUI.Demo.DocApiGen; + +/// +/// Loads documentation target assemblies and their dependencies in an isolated context. +/// +public sealed class DocumentationAssemblyLoadContext : AssemblyLoadContext +{ + private readonly Dictionary _inputAssemblyPaths; + private readonly IReadOnlyList _resolvers; + + /// + /// Initializes a new instance of the class. + /// + /// The assembly paths that can be loaded as documentation inputs. + public DocumentationAssemblyLoadContext(IEnumerable assemblyPaths) + : base(nameof(DocumentationAssemblyLoadContext), isCollectible: false) + { + _inputAssemblyPaths = assemblyPaths + .ToDictionary( + path => Path.GetFileNameWithoutExtension(path), + StringComparer.OrdinalIgnoreCase); + +#pragma warning disable CA1416 // Validate platform compatibility + _resolvers = _inputAssemblyPaths.Values + .Select(path => new AssemblyDependencyResolver(path)) + .ToArray(); +#pragma warning restore CA1416 // Validate platform compatibility + } + + /// + /// Resolves managed assemblies for the documentation inputs and their dependencies. + /// + /// The assembly name to resolve. + /// The loaded assembly if resolution succeeds; otherwise, . + protected override Assembly? Load(AssemblyName assemblyName) + { + if (!string.IsNullOrEmpty(assemblyName.Name) && _inputAssemblyPaths.TryGetValue(assemblyName.Name, out var inputAssemblyPath)) + { + return LoadFromAssemblyPath(inputAssemblyPath); + } + + foreach (var resolver in _resolvers) + { +#pragma warning disable CA1416 // Validate platform compatibility + var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); +#pragma warning restore CA1416 // Validate platform compatibility + if (!string.IsNullOrEmpty(assemblyPath)) + { + return LoadFromAssemblyPath(assemblyPath); + } + } + + return null; + } + + /// + /// Resolves unmanaged libraries for the documentation inputs and their dependencies. + /// + /// The unmanaged library name to resolve. + /// A native library handle if resolution succeeds; otherwise, 0. + protected override nint LoadUnmanagedDll(string unmanagedDllName) + { + foreach (var resolver in _resolvers) + { +#pragma warning disable CA1416 // Validate platform compatibility + var unmanagedDllPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); +#pragma warning restore CA1416 // Validate platform compatibility + if (!string.IsNullOrEmpty(unmanagedDllPath)) + { + return LoadUnmanagedDllFromPath(unmanagedDllPath); + } + } + + return 0; + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs index b9bace2286..7f54bfe94d 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/AllDocumentationGenerator.cs @@ -6,6 +6,7 @@ using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models; using FluentUI.Demo.DocApiGen.Models.AllMode; using FluentUI.Demo.DocApiGen.Models.SummaryMode; @@ -17,17 +18,16 @@ namespace FluentUI.Demo.DocApiGen.Generators; /// public sealed class AllDocumentationGenerator : DocumentationGeneratorBase { - private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + private readonly DocumentationCommentProvider _commentProvider; /// /// Initializes a new instance of the class. /// - /// The assembly to generate documentation for. - /// The XML documentation file. - public AllDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) - : base(assembly, xmlDocumentation) + /// The documentation inputs to generate documentation for. + public AllDocumentationGenerator(IReadOnlyList inputs) + : base(inputs) { - _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + _commentProvider = new DocumentationCommentProvider(inputs); } /// @@ -57,8 +57,9 @@ private DocumentationRoot BuildDocumentationData() var components = new List(); var enums = new List(); - var validTypes = Assembly.GetTypes().Where(IsValidComponentType).ToList(); - var enumTypes = Assembly.GetTypes().Where(t => t.IsEnum && t.IsPublic).ToList(); + var allTypes = Inputs.SelectMany(input => input.Assembly.GetTypes()).ToList(); + var validTypes = allTypes.Where(IsValidComponentType).ToList(); + var enumTypes = allTypes.Where(t => t.IsEnum && t.IsPublic).ToList(); Console.WriteLine($"Processing {validTypes.Count} components and {enumTypes.Count} enums..."); @@ -112,7 +113,7 @@ private DocumentationRoot BuildDocumentationData() { try { - var options = new ApiClassOptions(Assembly, _docXmlReader) + var options = new ApiClassOptions(type.Assembly, _commentProvider) { Mode = GenerationMode.All }; @@ -211,7 +212,7 @@ private EnumInfo GenerateEnumInfo(Type type) var name = names[i]; var value = Convert.ToInt32(enumValues.GetValue(i), CultureInfo.InvariantCulture); var field = type.GetField(name); - var description = field != null ? _docXmlReader.GetMemberSummary(field) : string.Empty; + var description = field != null ? _commentProvider.GetMemberSummary(field) : string.Empty; values.Add(new EnumValueInfo { @@ -221,7 +222,7 @@ private EnumInfo GenerateEnumInfo(Type type) }); } - var enumDescription = _docXmlReader.GetComponentSummary(type); + var enumDescription = _commentProvider.GetComponentSummary(type); return new EnumInfo { @@ -314,7 +315,7 @@ private static string DetermineCategory(Type type) { var version = "Unknown"; - var versionAttribute = Assembly.GetCustomAttribute(); + var versionAttribute = PrimaryAssembly.GetCustomAttribute(); if (versionAttribute != null) { var versionString = versionAttribute.InformationalVersion; diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs index 3fa2a1a780..d616f72afb 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorBase.cs @@ -4,6 +4,7 @@ using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Models; namespace FluentUI.Demo.DocApiGen.Generators; @@ -13,29 +14,34 @@ namespace FluentUI.Demo.DocApiGen.Generators; public abstract class DocumentationGeneratorBase : IDocumentationGenerator { /// - /// Represents the assembly associated with the current context or operation. + /// Represents the documentation inputs associated with the current operation. /// - protected readonly Assembly Assembly; + protected readonly IReadOnlyList Inputs; /// - /// Represents the XML documentation file associated with the assembly. + /// Gets the primary documentation input. /// - protected readonly FileInfo XmlDocumentation; + protected DocumentationInput PrimaryInput => Inputs[0]; + + /// + /// Gets the primary assembly. + /// + protected Assembly PrimaryAssembly => PrimaryInput.Assembly; /// /// Initializes a new instance of the class. /// - /// The assembly to generate documentation for. - /// The XML documentation file. - protected DocumentationGeneratorBase(Assembly assembly, FileInfo xmlDocumentation) + /// The documentation inputs to generate documentation for. + protected DocumentationGeneratorBase(IReadOnlyList inputs) { - Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); - XmlDocumentation = xmlDocumentation ?? throw new ArgumentNullException(nameof(xmlDocumentation)); + ArgumentNullException.ThrowIfNull(inputs); - if (!xmlDocumentation.Exists) + if (inputs.Count == 0) { - throw new FileNotFoundException($"XML documentation file not found: {xmlDocumentation.FullName}"); + throw new ArgumentException("At least one documentation input is required.", nameof(inputs)); } + + Inputs = inputs; } /// diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs index 99ee180e8e..2784e698b3 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/DocumentationGeneratorFactory.cs @@ -4,6 +4,7 @@ using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Models; namespace FluentUI.Demo.DocApiGen.Generators; @@ -12,6 +13,31 @@ namespace FluentUI.Demo.DocApiGen.Generators; /// public static class DocumentationGeneratorFactory { + /// + /// Creates a documentation generator for the specified mode. + /// + /// The generation mode. + /// The documentation inputs to generate documentation for. + /// A documentation generator instance. + /// Thrown when inputs is null. + /// Thrown when the mode is not supported. + public static IDocumentationGenerator Create( + GenerationMode mode, + IReadOnlyList inputs) + { + ArgumentNullException.ThrowIfNull(inputs); + + return mode switch + { + GenerationMode.Summary => new SummaryDocumentationGenerator(inputs), + GenerationMode.All => new AllDocumentationGenerator(inputs), + GenerationMode.Mcp => new McpDocumentationGenerator(inputs), + GenerationMode.Icons => new IconsEmojisGenerator(inputs, mode), + GenerationMode.Emojis => new IconsEmojisGenerator(inputs, mode), + _ => throw new NotSupportedException($"Generation mode '{mode}' is not supported.") + }; + } + /// /// Creates a documentation generator for the specified mode. /// @@ -19,26 +45,15 @@ public static class DocumentationGeneratorFactory /// The assembly to generate documentation for. /// The XML documentation file. /// A documentation generator instance. - /// Thrown when assembly or xmlDocumentation is null. - /// Thrown when the mode is not supported. public static IDocumentationGenerator Create( GenerationMode mode, Assembly assembly, FileInfo xmlDocumentation) { ArgumentNullException.ThrowIfNull(assembly); - ArgumentNullException.ThrowIfNull(xmlDocumentation); - return mode switch - { - GenerationMode.Summary => new SummaryDocumentationGenerator(assembly, xmlDocumentation), - GenerationMode.All => new AllDocumentationGenerator(assembly, xmlDocumentation), - GenerationMode.Mcp => new McpDocumentationGenerator(assembly, xmlDocumentation), - GenerationMode.Icons => new IconsEmojisGenerator(assembly, xmlDocumentation, mode), - GenerationMode.Emojis => new IconsEmojisGenerator(assembly, xmlDocumentation, mode), - _ => throw new NotSupportedException($"Generation mode '{mode}' is not supported.") - }; + return Create(mode, [new DocumentationInput(assembly, xmlDocumentation)]); } /// diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/IconsEmojisGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/IconsEmojisGenerator.cs index 09d6853c63..eafe98210d 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/IconsEmojisGenerator.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/IconsEmojisGenerator.cs @@ -2,9 +2,9 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using System.Reflection; using System.Text.Json; using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Models; namespace FluentUI.Demo.DocApiGen.Generators; @@ -18,11 +18,10 @@ public sealed class IconsEmojisGenerator : DocumentationGeneratorBase /// /// Initializes a new instance of the class. /// - /// The assembly to generate documentation for. - /// The XML documentation file. + /// The documentation inputs to generate documentation for. /// The generation mode (Icons or Emojis). - public IconsEmojisGenerator(Assembly assembly, FileInfo xmlDocumentation, GenerationMode mode) - : base(assembly, xmlDocumentation) + public IconsEmojisGenerator(IReadOnlyList inputs, GenerationMode mode) + : base(inputs) { _mode = mode; } diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/McpDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/McpDocumentationGenerator.cs index 75da9dc21a..ff86a7eaf0 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/McpDocumentationGenerator.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/McpDocumentationGenerator.cs @@ -6,7 +6,7 @@ using System.Globalization; using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; -using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models; using FluentUI.Demo.DocApiGen.Models.McpMode; namespace FluentUI.Demo.DocApiGen.Generators; @@ -17,7 +17,7 @@ namespace FluentUI.Demo.DocApiGen.Generators; /// public sealed class McpDocumentationGenerator : DocumentationGeneratorBase { - private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + private readonly DocumentationCommentProvider _commentProvider; // MCP attribute type names (we check by name to avoid assembly dependency) private const string McpServerToolTypeAttribute = "McpServerToolTypeAttribute"; @@ -30,12 +30,11 @@ public sealed class McpDocumentationGenerator : DocumentationGeneratorBase /// /// Initializes a new instance of the class. /// - /// The assembly to generate documentation for. - /// The XML documentation file. - public McpDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) - : base(assembly, xmlDocumentation) + /// The documentation inputs to generate documentation for. + public McpDocumentationGenerator(IReadOnlyList inputs) + : base(inputs) { - _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + _commentProvider = new DocumentationCommentProvider(inputs); } /// @@ -66,7 +65,10 @@ private McpDocumentationRoot BuildMcpDocumentationData() var resources = new List(); var prompts = new List(); - var allTypes = Assembly.GetTypes().Where(t => t.IsClass && t.IsPublic && !t.IsAbstract).ToList(); + var allTypes = Inputs + .SelectMany(input => input.Assembly.GetTypes()) + .Where(t => t.IsClass && t.IsPublic && !t.IsAbstract) + .ToList(); Console.WriteLine($"Scanning {allTypes.Count} types for MCP attributes..."); @@ -125,7 +127,7 @@ private McpDocumentationRoot BuildMcpDocumentationData() { try { - var xmlSummary = _docXmlReader.GetMemberSummary(method); + var xmlSummary = _commentProvider.GetMemberSummary(method); var description = GetDescriptionAttribute(method); var parameters = ExtractMethodParameters(method); @@ -154,7 +156,7 @@ private McpDocumentationRoot BuildMcpDocumentationData() { try { - var xmlSummary = _docXmlReader.GetMemberSummary(method); + var xmlSummary = _commentProvider.GetMemberSummary(method); var description = GetDescriptionAttribute(method); var resourceAttr = GetResourceAttributeProperties(method); @@ -192,7 +194,7 @@ private McpDocumentationRoot BuildMcpDocumentationData() { try { - var xmlSummary = _docXmlReader.GetMemberSummary(method); + var xmlSummary = _commentProvider.GetMemberSummary(method); var description = GetDescriptionAttribute(method); var parameters = ExtractMethodParameters(method); @@ -355,7 +357,7 @@ private static bool IsNullableType(Type type) { var version = "Unknown"; - var versionAttribute = Assembly.GetCustomAttribute(); + var versionAttribute = PrimaryAssembly.GetCustomAttribute(); if (versionAttribute != null) { var versionString = versionAttribute.InformationalVersion; diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs index 7466fa6d36..73a7e63f52 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Generators/SummaryDocumentationGenerator.cs @@ -6,6 +6,7 @@ using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; using FluentUI.Demo.DocApiGen.Extensions; +using FluentUI.Demo.DocApiGen.Models; using FluentUI.Demo.DocApiGen.Models.SummaryMode; namespace FluentUI.Demo.DocApiGen.Generators; @@ -16,17 +17,16 @@ namespace FluentUI.Demo.DocApiGen.Generators; /// public sealed class SummaryDocumentationGenerator : DocumentationGeneratorBase { - private readonly LoxSmoke.DocXml.DocXmlReader _docXmlReader; + private readonly DocumentationCommentProvider _commentProvider; /// /// Initializes a new instance of the class. /// - /// The assembly to generate documentation for. - /// The XML documentation file. - public SummaryDocumentationGenerator(Assembly assembly, FileInfo xmlDocumentation) - : base(assembly, xmlDocumentation) + /// The documentation inputs to generate documentation for. + public SummaryDocumentationGenerator(IReadOnlyList inputs) + : base(inputs) { - _docXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + _commentProvider = new DocumentationCommentProvider(inputs); } /// @@ -49,13 +49,32 @@ private SummaryDocumentationData BuildDocumentationData() var (version, date) = GetAssemblyInfo(); var components = new Dictionary(); - var options = new ApiClassOptions(Assembly, _docXmlReader) + var options = new ApiClassOptions(PrimaryAssembly, _commentProvider) { Mode = GenerationMode.Summary, PropertyParameterOnly = false, }; - var validTypes = Assembly.GetTypes().Where(t => t.IsValidType()).ToList(); + var validTypes = Inputs + .SelectMany(input => + { + try + { + return input.Assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + Console.WriteLine($"[WARNING] Could not load all types from {input.Assembly.FullName}"); + foreach (var loaderException in ex.LoaderExceptions.Where(e => e is not null)) + { + Console.WriteLine($" - {loaderException!.Message}"); + } + + return ex.Types.OfType(); + } + }) + .Where(t => t.IsValidType()) + .ToList(); Console.WriteLine($"Processing {validTypes.Count} valid types..."); var processedCount = 0; @@ -123,7 +142,7 @@ private SummaryDocumentationData BuildDocumentationData() { var version = "Unknown"; - var versionAttribute = Assembly.GetCustomAttribute(); + var versionAttribute = PrimaryAssembly.GetCustomAttribute(); if (versionAttribute != null) { var versionString = versionAttribute.InformationalVersion; diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationCommentProvider.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationCommentProvider.cs new file mode 100644 index 0000000000..6b21af2dd0 --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationCommentProvider.cs @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using FluentUI.Demo.DocApiGen.Abstractions; +using FluentUI.Demo.DocApiGen.Extensions; + +namespace FluentUI.Demo.DocApiGen.Models; + +/// +/// Resolves XML documentation comments across one or more documentation inputs. +/// +public sealed class DocumentationCommentProvider : IDocumentationCommentProvider +{ + private readonly IReadOnlyDictionary _inputsByAssembly; + private readonly IReadOnlyList _inputs; + + /// + /// Initializes a new instance of the class. + /// + /// The documentation inputs to resolve comments from. + public DocumentationCommentProvider(IReadOnlyList inputs) + { + ArgumentNullException.ThrowIfNull(inputs); + + if (inputs.Count == 0) + { + throw new ArgumentException("At least one documentation input is required.", nameof(inputs)); + } + + _inputs = inputs; + _inputsByAssembly = inputs.ToDictionary(input => input.Assembly); + } + + /// + public string GetComponentSummary(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + return TryGetInput(type.Assembly, out var input) + ? input.DocXmlReader.GetComponentSummary(type) + : string.Empty; + } + + /// + public string GetMemberSummary(MemberInfo member) + { + ArgumentNullException.ThrowIfNull(member); + + var assembly = member.Module.Assembly; + return TryGetInput(assembly, out var input) + ? input.DocXmlReader.GetMemberSummary(member) + : string.Empty; + } + + private bool TryGetInput(Assembly assembly, out DocumentationInput input) + { + if (_inputsByAssembly.TryGetValue(assembly, out input!)) + { + return true; + } + + input = _inputs[0]; + return false; + } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationInput.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationInput.cs new file mode 100644 index 0000000000..ab076e4e4a --- /dev/null +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/DocumentationInput.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Reflection; + +namespace FluentUI.Demo.DocApiGen.Models; + +/// +/// Represents one documentation source consisting of an assembly and its XML documentation file. +/// +public sealed class DocumentationInput +{ + /// + /// Initializes a new instance of the class. + /// + /// The assembly to document. + /// The XML documentation file associated with the assembly. + public DocumentationInput(Assembly assembly, FileInfo xmlDocumentation) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + XmlDocumentation = xmlDocumentation ?? throw new ArgumentNullException(nameof(xmlDocumentation)); + + if (!xmlDocumentation.Exists) + { + throw new FileNotFoundException($"XML documentation file not found: {xmlDocumentation.FullName}"); + } + + DocXmlReader = new LoxSmoke.DocXml.DocXmlReader(xmlDocumentation.FullName); + } + + /// + /// Gets the assembly to document. + /// + public Assembly Assembly { get; } + + /// + /// Gets the XML documentation file associated with the assembly. + /// + public FileInfo XmlDocumentation { get; } + + /// + /// Gets the XML documentation reader for the assembly. + /// + public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } +} diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs index d64087d26b..cd7c35786f 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClass.cs @@ -328,8 +328,8 @@ private static bool CanCreateInstance(Type type) private string GetSummary(Type component, MemberInfo? member) { return member == null - ? _options.DocXmlReader.GetComponentSummary(component) - : _options.DocXmlReader.GetMemberSummary(member); + ? _options.CommentProvider.GetComponentSummary(component) + : _options.CommentProvider.GetMemberSummary(member); } /// diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs index 287a7c7d41..ce8e844a17 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Models/SummaryMode/ApiClassOptions.cs @@ -16,11 +16,11 @@ public class ApiClassOptions /// Initializes a new instance of the class. /// /// - /// - public ApiClassOptions(Assembly assembly, LoxSmoke.DocXml.DocXmlReader docReader) + /// + public ApiClassOptions(Assembly assembly, IDocumentationCommentProvider commentProvider) { Assembly = assembly; - DocXmlReader = docReader; + CommentProvider = commentProvider; } /// @@ -29,9 +29,9 @@ public ApiClassOptions(Assembly assembly, LoxSmoke.DocXml.DocXmlReader docReader public Assembly Assembly { get; } /// - /// Gets the summary reader. + /// Gets the summary provider. /// - public LoxSmoke.DocXml.DocXmlReader DocXmlReader { get; } + public IDocumentationCommentProvider CommentProvider { get; } /// /// Gets or sets whether to include all properties (false) or only those with [Parameter] attribute (true). diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs index 2985a73f78..df0507f31d 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Program.cs @@ -2,11 +2,12 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using System.Reflection; using FluentUI.Demo.DocApiGen.Abstractions; using FluentUI.Demo.DocApiGen.Formatters; using FluentUI.Demo.DocApiGen.Generators; +using FluentUI.Demo.DocApiGen.Models; using Microsoft.Extensions.Configuration; -using System.Reflection; namespace FluentUI.Demo.DocApiGen; @@ -16,6 +17,7 @@ namespace FluentUI.Demo.DocApiGen; public class Program { private static readonly System.Diagnostics.Stopwatch _watcher = new(); + private static readonly char[] InputSeparators = [';', '|']; /// /// Main method. @@ -32,23 +34,19 @@ public static void Main(string[] args) // Build a configuration object from command line var config = new ConfigurationBuilder().AddCommandLine(args).Build(); - var xmlFile = config["xml"]; - var dllFile = config["dll"]; + var xmlFiles = config["xml"]; + var dllFiles = config["dll"]; var outputFile = config["output"]; var format = config["format"] ?? "json"; var modeArg = config["mode"] ?? "summary"; // Help - if (string.IsNullOrEmpty(xmlFile) || string.IsNullOrEmpty(dllFile)) + if (string.IsNullOrEmpty(xmlFiles) || string.IsNullOrEmpty(dllFiles)) { ShowHelp(); return; } - // Ensure dllFile and xmlFile are absolute paths - dllFile = Path.IsPathRooted(dllFile) ? dllFile : Path.Combine(Directory.GetCurrentDirectory(), dllFile); - xmlFile = Path.IsPathRooted(xmlFile) ? xmlFile : Path.Combine(Directory.GetCurrentDirectory(), xmlFile); - try { // Parse generation mode @@ -57,18 +55,17 @@ public static void Main(string[] args) // Validate format compatibility ValidateFormatCompatibility(mode, format); - // Load assembly and XML documentation - var assembly = Assembly.LoadFile(dllFile); - var docXml = new FileInfo(xmlFile); + // Load assemblies and XML documentation + var inputs = CreateDocumentationInputs(dllFiles, xmlFiles); Console.WriteLine("Generating documentation..."); - Console.WriteLine($" Assembly: {assembly.GetName().Name}"); + Console.WriteLine($" Assemblies: {string.Join(", ", inputs.Select(input => input.Assembly.GetName().Name))}"); Console.WriteLine($" Mode: {mode}"); Console.WriteLine($" Format: {format}"); Console.WriteLine(); // Create generator and formatter - var generator = DocumentationGeneratorFactory.Create(mode, assembly, docXml); + var generator = DocumentationGeneratorFactory.Create(mode, inputs); var formatter = OutputFormatterFactory.Create(format); // Generate and output @@ -101,18 +98,18 @@ public static void Main(string[] args) Console.ResetColor(); } - Environment.Exit(1); + //Environment.Exit(1); } } private static void ShowHelp() { Console.WriteLine("Usage:"); - Console.WriteLine(" DocApiGen --xml --dll [options]"); + Console.WriteLine(" DocApiGen --xml --dll [options]"); Console.WriteLine(); Console.WriteLine("Required Arguments:"); - Console.WriteLine(" --xml Path to the XML documentation file"); - Console.WriteLine(" --dll Path to the assembly DLL file"); + Console.WriteLine(" --xml Path to the XML documentation file, or multiple paths separated by ';' or '|'"); + Console.WriteLine(" --dll Path to the assembly DLL file, or multiple paths separated by ';' or '|'"); Console.WriteLine(); Console.WriteLine("Optional Arguments:"); Console.WriteLine(" --output Path to the output file (default: stdout)"); @@ -145,6 +142,9 @@ private static void ShowHelp() Console.WriteLine(" # Generate All mode JSON"); Console.WriteLine(" DocApiGen --xml MyApp.xml --dll MyApp.dll --output api-all.json --mode all"); Console.WriteLine(); + Console.WriteLine(" # Generate Summary mode JSON from multiple assemblies"); + Console.WriteLine(" DocApiGen --xml Core.xml;Charts.xml --dll Core.dll;Charts.dll --output api-summary.json"); + Console.WriteLine(); Console.WriteLine(" # Generate MCP documentation JSON"); Console.WriteLine(" DocApiGen --xml McpServer.xml --dll McpServer.dll --output mcp-docs.json --mode mcp"); Console.WriteLine(); @@ -193,4 +193,60 @@ private static void ValidateFormatCompatibility(GenerationMode mode, string form $"Mode '{mode}' only supports JSON format. Requested format: {format}"); } } + + private static IReadOnlyList CreateDocumentationInputs(string dllFiles, string xmlFiles) + { + var dllPaths = SplitInputValues(dllFiles) + .Select(MakeAbsolutePath) + .ToArray(); + + var xmlPaths = SplitInputValues(xmlFiles) + .Select(MakeAbsolutePath) + .ToArray(); + + if (dllPaths.Length != xmlPaths.Length) + { + throw new ArgumentException($"The number of DLL files ({dllPaths.Length}) must match the number of XML files ({xmlPaths.Length})."); + } + + var loadContext = new DocumentationAssemblyLoadContext(dllPaths); + + return dllPaths + .Select((dllPath, index) => + { + Console.WriteLine($"Attempting to load assembly: {dllPath}"); + + try + { + var assembly = loadContext.LoadFromAssemblyPath(dllPath); + + Console.WriteLine($" Loaded input assembly: {assembly.GetName().Name}"); + Console.WriteLine($" Requested path: {dllPath}"); + Console.WriteLine($" FullName: {assembly.FullName}"); + Console.WriteLine($" Location: {assembly.Location}"); + + return new DocumentationInput(assembly, new FileInfo(xmlPaths[index])); + } + catch (FileLoadException ex) + { + Console.WriteLine($" Failed to load assembly: {dllPath}"); + Console.WriteLine($" {ex.Message}"); + throw; + } + }) + .ToArray(); + } + + private static IEnumerable SplitInputValues(string value) + { + return value + .Split(InputSeparators, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Where(static path => !string.IsNullOrWhiteSpace(path)); + } + private static string MakeAbsolutePath(string path) + { + return Path.IsPathRooted(path) + ? path + : Path.Combine(Directory.GetCurrentDirectory(), path); + } } diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs index 99be0c8dfb..4f377c93e1 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs @@ -139,11 +139,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } // Get the component type - var type = DocViewerService.ApiAssembly - ?.GetTypes() - ?.FirstOrDefault(i => i.Name == componentName - || i.Name.StartsWith($"{componentName}`1") - || i.Name.StartsWith($"{componentName}`2")); + var type = DocViewerService.FindApiType(componentName); // Create the ApiClass var result = type is null ? null : new ApiClass(DocViewerService, type, allProperties); @@ -165,7 +161,6 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } } - result.InstanceTypes = listOfTypes.ToArray(); } diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs index 40d3723f70..b755bda33e 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerOptions.cs @@ -29,7 +29,17 @@ public class DocViewerOptions /// /// Assembly containing the API classes to display in API sections. /// - public Assembly? ApiAssembly { get; set; } + public IReadOnlyList ApiAssemblies { get; set; } = []; + + /// + /// Assembly containing the API classes to display in API sections. + /// + [Obsolete("Use ApiAssemblies instead.")] + public Assembly? ApiAssembly + { + get => ApiAssemblies.Count > 0 ? ApiAssemblies[0] : null; + set => ApiAssemblies = value is null ? [] : [value]; + } /// /// Function to get the summary of an API comment. diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs index f0dafbc461..98690e37d2 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Services/DocViewerService.cs @@ -30,7 +30,7 @@ public DocViewerService(DocViewerOptions options) Options = options; ComponentsAssembly = options.ComponentsAssembly; ResourcesAssembly = options.ResourcesAssembly; - ApiAssembly = options.ApiAssembly; + ApiAssemblies = options.ApiAssemblies; ApiCommentSummary = options.ApiCommentSummary; } @@ -50,9 +50,15 @@ public DocViewerService(DocViewerOptions options) public Assembly? ResourcesAssembly { get; } /// - /// Gets the assembly containing the classes to display in API sections. + /// Gets the assemblies containing the classes to display in API sections. /// - public Assembly? ApiAssembly { get; } + public IReadOnlyList ApiAssemblies { get; } + + /// + /// Gets the primary assembly containing the classes to display in API sections. + /// + [Obsolete("Use ApiAssemblies instead.")] + public Assembly? ApiAssembly => ApiAssemblies.Count > 0 ? ApiAssemblies[0] : null; /// /// Function to get the summary of an API comment. @@ -64,6 +70,33 @@ public DocViewerService(DocViewerOptions options) /// public IEnumerable Pages => _pages ??= LoadAllPages(); + /// + /// Returns the associated to the . + /// + /// The full name of the type. + /// The if found; otherwise, null. + public Type? FindApiType(string fullName) + { + if (string.IsNullOrWhiteSpace(fullName)) + { + return null; + } + + foreach (var assembly in ApiAssemblies) + { + var type = assembly.GetType(fullName, throwOnError: false, ignoreCase: false); + if (type is not null) + { + return type; + } + } + + return ApiAssemblies + .SelectMany(GetTypes) + .FirstOrDefault(type => string.Equals(type.FullName, fullName, StringComparison.Ordinal) + || string.Equals(type.Name, fullName, StringComparison.Ordinal)); + } + /// /// Returns the associated to the . /// Or null if not found. @@ -146,4 +179,17 @@ private IEnumerable LoadAllPages() return pages.Where(i => !string.IsNullOrEmpty(i.Route)).OrderBy(i => i.Route); } + + /// + private static IEnumerable GetTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.OfType(); + } + } }