Skip to content

Add unified platform map for cross-platform API documentation#4327

Open
AlexDaines wants to merge 8 commits intodevelopmentfrom
adaines/unified-platform-map
Open

Add unified platform map for cross-platform API documentation#4327
AlexDaines wants to merge 8 commits intodevelopmentfrom
adaines/unified-platform-map

Conversation

@AlexDaines
Copy link
Copy Markdown
Contributor

@AlexDaines AlexDaines commented Feb 5, 2026

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

  • Built doc generator: dotnet build SDKDocGenerator/SDKDocGenerator.csproj
  • Generated test docs for BedrockRuntime and S3 services
  • Verified H2 exclusive method pages generated (InvokeModelWithBidirectionalStreamAsync)
  • Verified Version Information section unchanged
  • No new build errors (only expected obsolete warnings)

Breaking 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have read the README document
  • I have added tests to cover my changes
  • All new and existing tests passed

License

  • I confirm that this pull request can be released under the Apache 2 license

@AlexDaines AlexDaines requested a review from Copilot February 5, 2026 18:10
@AlexDaines AlexDaines marked this pull request as ready for review February 5, 2026 18:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AssemblyLoadContext to 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)

@AlexDaines AlexDaines marked this pull request as draft February 5, 2026 18:32
@AlexDaines AlexDaines force-pushed the adaines/unified-platform-map branch from 70f14c9 to a2ed6fa Compare March 17, 2026 17:59
@AlexDaines AlexDaines requested a review from Copilot March 19, 2026 17:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 11 comments.

@AlexDaines AlexDaines marked this pull request as ready for review March 20, 2026 17:10
@AlexDaines AlexDaines requested a review from boblodgett March 23, 2026 20:26
@GarrettBeatty GarrettBeatty self-requested a review March 26, 2026 23:56
{
// First pass: Scan primary platform to establish baseline signatures
var primaryAssemblyPath = Path.GetFullPath(Path.Combine(_options.SDKAssembliesRoot, primaryPlatform, assemblyName + ".dll"));
if (File.Exists(primaryAssemblyPath))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why it wouldnt exist here? should we be throwing error if it doesnt exist instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +73 to +126
// 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}");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question/suggestion: i think currently the way this works is something like

  1. (GenerationManifest.Generate()WriteType()new ClassWriter(this, version, type).Write()) generates the class page with only net472 methods.

  2. (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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@AlexDaines AlexDaines force-pushed the adaines/unified-platform-map branch from b6ee456 to 2cae95d Compare March 31, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants