Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions src/Aspire.Hosting.RemoteHost/AssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,25 @@ private static List<Assembly> LoadAssemblies(
/// libs directory at a different identity than what the default context provides.
/// </summary>
/// <remarks>
/// <para>
/// This is a defense against a real failure mode: when the bundled <c>Aspire.TypeSystem</c>
/// (compiled into the apphost server's single-file executable) and the libs copy
/// (restored alongside <c>Aspire.Hosting.*.dll</c>) report different assembly versions or MVIDs,
/// integration assemblies that reference the libs copy will fail to bind their type
/// references through the default context. The resulting <see cref="ReflectionTypeLoadException"/>
/// would otherwise be swallowed silently and surface only as a downstream "no code generator
/// found" / "no language support found" error with no actionable diagnostic.
/// (compiled into the apphost server's single-file executable) and the libs copy (restored
/// alongside <c>Aspire.Hosting.*.dll</c>) report different assembly versions or MVIDs,
/// integration assemblies that reference the libs copy will fail to bind their type references
/// through the default context. The resulting <see cref="ReflectionTypeLoadException"/> would
/// otherwise be swallowed silently and surface only as a downstream "no code generator found"
/// or "no language support found" error with no actionable diagnostic.
/// </para>
/// <para>
/// <c>Aspire.TypeSystem</c> freezes its strong-name <c>AssemblyVersion</c> at a fixed constant
/// (see <c>src/Aspire.TypeSystem/Aspire.TypeSystem.csproj</c> for the full rationale). The CLR
/// satisfies a strong-named reference when the loaded (bundled) copy's version is <c>&gt;=</c>
/// the requested version, so the only failing configuration is a bundled copy STRICTLY LOWER
/// than the libs copy -- an already-shipped old CLI paired with post-freeze libs. The warning
/// below trips only on that case; the supported "new CLI + older SDK" direction (bundled
/// <c>&gt;=</c> libs, including a same-version/differing-MVID daily-SDK pairing) binds and is
/// logged at Debug only.
/// </para>
/// </remarks>
private static void WarnIfSharedAssemblyMismatch(string? integrationLibsPath, ILogger logger)
{
Expand Down Expand Up @@ -271,26 +283,29 @@ private static void WarnIfSharedAssemblyMismatch(string? integrationLibsPath, IL
var defaultName = defaultAsm.GetName();
var defaultMvid = defaultAsm.ManifestModule.ModuleVersionId;

if (defaultName.Version != probedName.Version)
if (defaultName.Version < probedName.Version)
{
// Bundled copy strictly lower: it cannot satisfy the libs' strong-named reference,
// so integrations are silently skipped during type discovery -- surface it.
logger.LogWarning(
"Shared assembly '{AssemblyName}' version mismatch: bundled={BundledVersion}, libs={LibsVersion} ({LibsPath}). " +
"Integration assemblies referencing this assembly from the libs directory will fail to bind their type " +
"references through the default load context, which causes integrations to be silently skipped during type discovery. " +
"This typically indicates the apphost server bundle and the restored integration packages were produced by " +
"different build configurations.",
"Shared assembly '{AssemblyName}' version too low: bundled={BundledVersion}, libs={LibsVersion} ({LibsPath}). " +
"The bundled copy is older than the integration packages and cannot satisfy their strong-named reference, " +
"so integration assemblies will fail to bind their type references through the default load context and be " +
"silently skipped during type discovery. The apphost server (CLI) is older than the restored Aspire packages; " +
"update the Aspire CLI to a version at least as new as the packages.",
sharedName,
defaultName.Version,
probedName.Version,
libsPath);
continue;
}

// Same version, but different MVID (compiled from different sources) is also a binary-incompatibility risk.
// We can't read the probed MVID without loading the assembly, which we deliberately don't do here.
// Logging the bundled MVID at Debug helps correlate with any subsequent ReflectionTypeLoadException.
logger.LogDebug("Shared assembly '{AssemblyName}' identity matches: Version={Version}, BundledMvid={Mvid}",
sharedName, defaultName.Version, defaultMvid);
// Bundled version >= libs version: the bundled copy satisfies the libs' strong-named
// reference (strong-name binding keys on version + public key token, not MVID), so it
// binds. Logged at Debug to correlate with any unrelated ReflectionTypeLoadException;
// we can't read the probed MVID without loading the assembly, which we avoid here.
logger.LogDebug("Shared assembly '{AssemblyName}' satisfies libs reference: bundled={BundledVersion}, libs={LibsVersion}, BundledMvid={Mvid}",
sharedName, defaultName.Version, probedName.Version, defaultMvid);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Hosting.RemoteHost/IntegrationLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ internal IntegrationLoadContext(string[] probeDirectories, IntegrationPackagePro
// contexts, so shared contracts (ICodeGenerator, ILanguageSupport,
// AtsContext, etc.) work across the ALC boundary without requiring
// reflection or marshalling.
//
// Binding to the bundled (CLI) copy is safe because Aspire.TypeSystem freezes its
// strong-name AssemblyVersion at a fixed constant so the bundled copy satisfies the
// strong-named reference of any stable SDK codegen assembly (#18110, #17910). See
// src/Aspire.TypeSystem/Aspire.TypeSystem.csproj for the full rationale and the one
// residual "update your CLI" case routed to the #18125 diagnostics.
return null;
}

Expand Down
59 changes: 59 additions & 0 deletions src/Aspire.TypeSystem/Aspire.TypeSystem.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,65 @@
<IsAotCompatible>true</IsAotCompatible>
<PackageTags>aspire ats typesystem polyglot</PackageTags>
<Description>Aspire Type System (ATS) APIs for Aspire polyglot support.</Description>

<!--
Aspire.TypeSystem is the shared contract assembly for polyglot code generation. It is
bundled into the CLI's single-file apphost server (aspire-managed) and loaded into that
process's DEFAULT AssemblyLoadContext. IntegrationLoadContext deliberately force-shares
this assembly from the default context so the cross-ALC contracts (ICodeGenerator,
ILanguageSupport, AtsContext) keep a single type identity across the boundary
(see src/Aspire.Hosting.RemoteHost/IntegrationLoadContext.cs and docs/specs/bundle.md).

The language codegen assemblies (Aspire.Hosting.CodeGeneration.*) are restored from the SDK
side and reference this assembly by strong name (StrongNameKeyId=Open). The CLR satisfies a
strong-named reference only when the loaded (here: bundled) copy's AssemblyVersion is GREATER
THAN OR EQUAL TO the requested version. If AssemblyVersion floated with the build, the version
on either side would diverge whenever the CLI and SDK were built at different times, the
reference would fail to bind, Assembly.GetTypes() would throw ReflectionTypeLoadException, and
every generator would be silently dropped -> "No code generator/language support found".
A floating version cannot satisfy BOTH skew directions at once: it
necessarily breaks either "new CLI + older Aspire.Hosting" (backward compat, bundle.md
"Scenario 2") or "old CLI + newer Aspire.Hosting".

Fix: FREEZE AssemblyVersion at a fixed constant decoupled from the build, carried forward to all
later releases so every stable and servicing release from this point onward (13.4.5, 13.4.6, 13.5,
...) bundles the same value. The constant must be GREATER THAN OR EQUAL TO the latest already-shipped
stable version so the bundled copy satisfies the (lower or equal, real) reference of any
already-released codegen assembly -> the common, must-work backward-compat direction (a current CLI
running an existing stable app) binds. 13.4.5.0 is chosen because it matches the AssemblyVersion of
the latest already-shipped stable (13.4.5, whose AssemblyVersion floated to 13.4.5.0): freezing at
that exact value means the already-released 13.4.5 CLI (which bundles floating 13.4.5.0) and every
post-freeze CLI and SDK carry the identical constant, so post-freeze CLI/SDK pairs always match
exactly and no skew matters. (A constant higher than the latest shipped stable would break that
already-released CLI, whose bundled copy would then be lower than the frozen reference.)

Two residual hard-bind failures remain, both routed to the "run aspire update"
diagnostics rather than fixable by the freeze value:
1. An already-shipped OLD CLI (bundling a real, lower version) paired with post-freeze
codegen referencing this constant. That is the genuinely unsupportable "the CLI predates
this build" case: it cannot be fixed for an immutable shipped CLI regardless of value.
2. A pre-freeze 13.5.0 PREVIEW app whose codegen still references a real 13.5.0.0
(> 13.4.5.0). Preview-channel skew is outside the supported stable matrix; those
previews are superseded by post-freeze builds carrying this constant.
(Post-freeze CLIs never hit case 1: they all bundle this constant and satisfy any post-freeze
reference.)

Bump this constant ONLY when the shared contract changes in a binary-incompatible way (a
member is removed or its signature changes), and then only to the then-current product
version (which is always >= this value); an additive change (new members only) keeps the same
value. A binary-incompatible change is caught by the repository's standard public API
compatibility tracking (the checked-in api/Aspire.TypeSystem.cs reference surface and
package baseline validation), which flags any change to the shipped surface, forcing a
deliberate decision about whether the change is additive (no bump) or breaking (bump).

Only AssemblyVersion is frozen; FileVersion, InformationalVersion, and the NuGet package
version remain build-derived so diagnostics keep real build identities. Aspire.TypeSystem is
consumed only via ProjectReference (no PackageReference consumers), so the frozen
AssemblyVersion is not a package contract anyone binds to by exact version. Because
13.4.5.0 is >= the package baseline version, package baseline validation (CP0003) still
passes, so no validation override is required.
-->
<AssemblyVersion>13.4.5.0</AssemblyVersion>
</PropertyGroup>

</Project>
Loading