Add unified platform map for cross-platform API documentation#4327
Add unified platform map for cross-platform API documentation#4327AlexDaines wants to merge 8 commits intodevelopmentfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive platform availability mapping infrastructure to the AWS .NET SDK documentation generator to track API availability across different .NET platforms (net472, netstandard2.0, netcoreapp3.1, net8.0). This addresses issue #3938 where customers encountered documented APIs that don't exist on their target platform.
Changes:
- Implements a unified platform availability map that scans all platforms upfront and maintains signature-to-platform mappings
- Adds isolated assembly loading via
AssemblyLoadContextto enable loading multiple platform versions of the same assembly simultaneously - Introduces infrastructure for generating documentation pages for platform-exclusive APIs (e.g., H2 bidirectional streaming methods only available in net8.0)
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| PlatformMap/*.cs | New infrastructure for tracking member availability across platforms, including map builder, entry objects, assembly contexts, and signature generation utilities |
| SdkDocGenerator.cs | Main execution flow updated to build platform maps upfront, generate exclusive content, and provide legacy fallback path |
| GenerationManifest.cs | Extended to support platform maps, supplemental manifests, and exclusive page generation |
| GeneratorOptions.cs | Added UseLegacySupplemental option with deprecation notice for rollback safety |
| ReflectionWrappers.cs | Added IsolatedAssemblyLoadContext for loading same-named assemblies from different platforms |
| NDocUtilities.cs | Extended signature generation helpers and added duplicate key protection for multi-platform doc loading |
| ClassWriter.cs | Updated to merge supplemental methods from other platforms into class documentation pages |
| BaseWriter.cs | Added platform display name mapping for user-friendly badge rendering |
| CommandLineParser.cs | Added CLI flag for legacy supplemental mode |
| SDKDocGeneratorLib.csproj | Added System.Runtime.Loader dependency for assembly isolation |
| sdkstyle.css | Minor formatting fix (removed trailing whitespace) |
docgenerator/SDKDocGeneratorLib/PlatformMap/PlatformMapBuilder.cs
Outdated
Show resolved
Hide resolved
70f14c9 to
a2ed6fa
Compare
docgenerator/SDKDocGeneratorLib/PlatformMap/PlatformMapBuilder.cs
Outdated
Show resolved
Hide resolved
| { | ||
| // First pass: Scan primary platform to establish baseline signatures | ||
| var primaryAssemblyPath = Path.GetFullPath(Path.Combine(_options.SDKAssembliesRoot, primaryPlatform, assemblyName + ".dll")); | ||
| if (File.Exists(primaryAssemblyPath)) |
There was a problem hiding this comment.
any reason why it wouldnt exist here? should we be throwing error if it doesnt exist instead
There was a problem hiding this comment.
was going back and forth on this. i changed it to throw FileNotFoundException for the primary assembly since its a requirement for the baseline docs, while still skipping supplemental platforms if needed.
| // First pass: Scan primary platform to establish baseline signatures | ||
| var primaryAssemblyPath = Path.GetFullPath(Path.Combine(_options.SDKAssembliesRoot, primaryPlatform, assemblyName + ".dll")); | ||
| if (File.Exists(primaryAssemblyPath)) | ||
| { | ||
| var context = LoadAndScanPlatform( | ||
| primaryAssemblyPath, | ||
| primaryPlatform, | ||
| serviceName, | ||
| memberIndex, | ||
| isPrimary: true); | ||
|
|
||
| if (context != null) | ||
| { | ||
| loadedContexts.Add(context); | ||
| scannedPlatforms.Add(primaryPlatform); | ||
| if (_options.Verbose) | ||
| Trace.WriteLine($" Scanned primary platform {primaryPlatform}: {memberIndex.Count} signatures"); | ||
| } | ||
| } | ||
|
|
||
| // Second pass: Scan supplemental platforms and capture wrappers for exclusive members | ||
| foreach (var platform in platforms) | ||
| { | ||
| if (platform.Equals(primaryPlatform, StringComparison.OrdinalIgnoreCase)) | ||
| continue; // Already scanned | ||
|
|
||
| var assemblyPath = Path.GetFullPath(Path.Combine(_options.SDKAssembliesRoot, platform, assemblyName + ".dll")); | ||
| if (!File.Exists(assemblyPath)) | ||
| { | ||
| if (_options.Verbose) | ||
| Trace.WriteLine($" Skipping {platform}: assembly not found"); | ||
| continue; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| var context = LoadAndScanPlatform( | ||
| assemblyPath, | ||
| platform, | ||
| serviceName, | ||
| memberIndex, | ||
| isPrimary: false); | ||
|
|
||
| if (context != null) | ||
| { | ||
| loadedContexts.Add(context); | ||
| scannedPlatforms.Add(platform); | ||
| if (_options.Verbose) | ||
| Trace.WriteLine($" Scanned {platform}: {memberIndex.Count} unique signatures total"); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Trace.WriteLine($" WARNING: Failed to scan {platform}: {ex.Message}"); |
There was a problem hiding this comment.
suggestion: although this works, one thing thats kind of funky here is having separate logic for primary vs other ones.
why not have one loop (similar to the for each loop that you have foreach (var platform in platforms)
and then just have isPrimary set on each loop by checking if its equal to _options.Platform?
also the second thing that is kind of weird is the function takes in a list of platforms but then uses some things set from options.Platform to get the primary one. wondering if it makes sense to instead update platformmap builder signature to
public PlatformAvailabilityMap BuildMap(
string serviceName,
string assemblyName,
IEnumerable<string> platformsToScan,
string primaryPlatform,
boolean isVerbose
)
{
and then you dont even need to pass in the GeneratorOptions into this class.
There was a problem hiding this comment.
this was a really helpful comment. merged into a single loop with isPrimary derived from the platform name, and BuildMap now takes primaryPlatform and isVerbose directly instead of the whole options object (SDKAssembliesRoot stays on the constructor since it doesn't change per time called)
| using SDKDocGenerator.Syntax; | ||
| using SDKDocGenerator.PlatformMap; | ||
| using SDKDocGenerator.Syntax; | ||
| using System.Collections.Generic; |
There was a problem hiding this comment.
question/suggestion: i think currently the way this works is something like
-
(
GenerationManifest.Generate()→WriteType()→new ClassWriter(this, version, type).Write()) generates the class page with only net472 methods. -
(
GenerationManifest.GenerateExclusivePagesFromMap()→new ClassWriter(this, version, primaryType, exclusiveMethods).Write()) generates the same class page again, overwriting the file, now with exclusive methods merged in.
and then classwriter is called in two ways
/ 1st cakk — normal, no supplemental methods
var writer = new ClassWriter(this, version, type);
writer.Write();
// 2nd call — with supplemental methods
var classWriter = new ClassWriter(this, version, primaryType, exclusiveMethods);
classWriter.Write();
but inside AddMethods theres some hidden branching with supplementalMethods branching.
wondering if its possible to instead just do
// Caller assembles the complete method list
var methods = type.GetMethodsToDocument().ToList();
var exclusive = PlatformMap?.GetExclusiveMethodsForType(type.FullName)
?? Enumerable.Empty<MethodInfoWrapper>();
methods.AddRange(exclusive);
// ClassWriter just renders what it's given
var writer = new ClassWriter(this, version, type, methods);
writer.Write();
and then you dont need to do a second pass
There was a problem hiding this comment.
the second reason for this question/suggestion is because im thinking classwriter should do exactly that - write stuff to the docs.
but in current state it has some business logic of deciding what to write, instead of just directly writing
The SDK doc generator previously used net472 as the sole source of all APIs. H2 eventstream APIs (e.g., TranscribeStreaming's StartTranscription) only exist in the net8.0 target and get zero API reference documentation. This adds a PlatformMap subsystem that scans multiple target frameworks and generates documentation for platform-exclusive APIs: - PlatformMapBuilder: Scans assemblies from each target framework using isolated assembly load contexts to build a cross-platform member index - PlatformAvailabilityMap: Queryable map of which members exist on which platforms, with wrapper-based lookup for page generation - PlatformAssemblyContext: Manages per-platform assembly loading with proper isolation and disposal - MemberSignature: Deterministic signature generation for cross-platform member identity comparison - PlatformMemberEntry: Per-member platform availability tracking Key design decisions: - Uses FullName comparison for cross-assembly type identity (Equals() fails across assembly load contexts) - Inherited members attributed to their declaring type, not every derived class, preventing false exclusive method proliferation - Per-service exception handling wraps map building for resilience - First-wins cache strategy in NDocUtilities prevents cross-platform assembly interference Also includes minor NDocUtilities improvement for rendering <list>, <item>, <term>, and <description> XML doc elements to HTML. Addresses DOTNET-8085. Design: DOTNET-8116.
Tests cover: - MemberSignature utility methods (GetMemberType, GetMemberName, GetDeclaringTypeName, ExtractMethodName) and delegation consistency - PlatformAvailabilityMap query behavior with hand-crafted test data: universal members, exclusive members, platform case insensitivity, statistics, disposal, and edge cases - NDoc signature generation gaps: array parameters, nested generics, parameterless and parameterized constructor signatures Adds InternalsVisibleTo for test access to internal PlatformAvailabilityMap constructor.
- Add null check for coreManifest before accessing its assembly context to prevent NullReferenceException when Core assembly is absent - Correct misleading "first-wins ensures primary platform precedence" comment: docIds are per-service+platform, so cache entries never collide across platforms; the guard is a de-duplication check for same (service, platform) pair
- Remove unused methodName variable in FindMethodInAssembly (PlatformMapBuilder) - Replace ad-hoc signature format with MemberSignature.ForMethod in ClassWriter supplemental method dedup (guards against null FullName on generic type params) - Revert unrelated using System addition in GeneratorOptions.cs - Revert whitespace-only changes in sdkstyle.css
- Prevent discarding ManifestAssemblyContext when platform map has exclusive members that need it for page generation (NullReferenceException) - Remove unused ExclusivePlatform property from PlatformMemberEntry - Simplify platform lookup in ResolveExclusiveMethodWrappers using FirstOrDefault (all platforms already filtered to non-primary) - Remove unused InfoVerbose overloads from SdkDocGenerator
b6ee456 to
2cae95d
Compare
Description
Adds PlatformAvailabilityMap infrastructure for tracking API availability across .NET platforms (net472, netstandard2.0, netcoreapp3.1, net8.0). Enables documentation of platform-exclusive APIs like H2 eventstream methods that only exist in net8.0.
Motivation and Context
Fixes #3938 - Customers see documented APIs that don't exist on their target platform, wasting time trying to use APIs that won't compile.
This PR ships the infrastructure only. Badge UX deferred to follow-up PR pending design review.
Testing
Dryruns (03/31/26):
dotnetv4 (running) - a4511f2c-a7d8-49ce-813a-f685abc46ed0
powershell5 (running) - 6c0420ce-829e-42f9-a3a3-560b5dd86684
dotnet build SDKDocGenerator/SDKDocGenerator.csprojBreaking Changes Assessment
None. This is additive infrastructure with no changes to existing doc output behavior.
Screenshots (if appropriate)
N/A - Badge rendering deferred to follow-up PR.
Types of changes
Checklist
License