Skip to content

Commit a68ad08

Browse files
sebastienrosCopilot
andcommitted
Raise Aspire.TypeSystem AssemblyVersion freeze to high sentinel 10001.0.0.0
The shipped 1.0.0.0 freeze fixed old-CLI + new-SDK skew but regressed the common backward-compat direction (new CLI + stable-channel SDK), where the bundled Aspire.TypeSystem (1.0.0.0) could not satisfy the codegen assemblies' strong-named reference to the real released version (e.g. 13.4.3.0). The CLR only binds when the loaded copy's version is >= the requested version, so a single frozen value must sit ABOVE every shipped real version to satisfy the must-work direction. Freeze at 10001.0.0.0 (a '1.0' seeded high; major slot is the breaking-change bump axis). - Aspire.TypeSystem.csproj: AssemblyVersion 1.0.0.0 -> 10001.0.0.0; drop DisablePackageBaselineValidation (10001 >= package baseline, so CP0003 passes); rewrite rationale comment. - IntegrationLoadContext: explain the bundled high copy satisfies any lower/equal reference; residual old-CLI case is #18125's actionable 'update your CLI' path. - AssemblyLoader.WarnIfSharedAssemblyMismatch: warn only when bundled < libs (the only failing order); bundled >= libs binds and logs at Debug. - AtsSharedContractSurfaceTests / SharedContractSurface: reframe the freeze guard around constant-version binding (content compat, not version) and reflow comments. Fixes the regression behind #18110 and #17910; complements the #18125 diagnostics, which still surface the genuine new-member case (already-shipped old CLI + post-freeze codegen) as an actionable error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cc22587 commit a68ad08

5 files changed

Lines changed: 112 additions & 92 deletions

File tree

src/Aspire.Hosting.RemoteHost/AssemblyLoader.cs

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -236,19 +236,22 @@ private static List<Assembly> LoadAssemblies(
236236
/// or "no language support found" error with no actionable diagnostic.
237237
/// </para>
238238
/// <para>
239-
/// Since <c>Aspire.TypeSystem</c> freezes its strong-name <c>AssemblyVersion</c> at a constant
240-
/// decoupled from the build (see <c>src/Aspire.TypeSystem/Aspire.TypeSystem.csproj</c>), the
241-
/// bundled and probed copies share an <see cref="AssemblyName.Version"/> for every CLI/SDK
242-
/// combination. The version-equality check below therefore only trips when that constant has
243-
/// been deliberately bumped for a binary-incompatible contract change, which is exactly the
244-
/// case where an older bundled copy cannot satisfy a newer reference and genuinely warrants a
245-
/// warning.
239+
/// <c>Aspire.TypeSystem</c> freezes its strong-name <c>AssemblyVersion</c> at a high
240+
/// constant (10001.0.0.0) decoupled from the build (see
241+
/// <c>src/Aspire.TypeSystem/Aspire.TypeSystem.csproj</c>). The CLR satisfies a strong-named
242+
/// reference when the loaded (bundled) copy's version is <c>&gt;=</c> the requested version,
243+
/// so what matters is the ORDER of the two versions, not mere inequality. The bundled
244+
/// high-versioned copy satisfies the (lower, real-versioned) reference of any already-released
245+
/// SDK codegen assembly, and matches any post-freeze copy exactly. The only failing
246+
/// configuration is a bundled copy whose version is STRICTLY LOWER than the libs copy -- an
247+
/// already-shipped old CLI (real, lower version) paired with post-freeze libs referencing the
248+
/// constant -- so the warning below trips only on that case.
246249
/// </para>
247250
/// <para>
248-
/// The same-version/differing-MVID case (for example, a daily SDK build paired with a released
249-
/// CLI build) is expected and harmless: strong-name binding keys on version + public key token,
250-
/// not MVID, so the bundled copy still satisfies the reference. It is logged at Debug only, to
251-
/// correlate with any unrelated <see cref="ReflectionTypeLoadException"/>.
251+
/// When the bundled version is greater than or equal to the libs version (the supported
252+
/// "new CLI + older SDK" backward-compat direction, or a same-version/differing-MVID pairing
253+
/// such as a daily SDK build with a released CLI), binding succeeds and it is logged at Debug
254+
/// only, to correlate with any unrelated <see cref="ReflectionTypeLoadException"/>.
252255
/// </para>
253256
/// </remarks>
254257
private static void WarnIfSharedAssemblyMismatch(string? integrationLibsPath, ILogger logger)
@@ -288,34 +291,38 @@ private static void WarnIfSharedAssemblyMismatch(string? integrationLibsPath, IL
288291
var defaultName = defaultAsm.GetName();
289292
var defaultMvid = defaultAsm.ManifestModule.ModuleVersionId;
290293

291-
if (defaultName.Version != probedName.Version)
294+
if (defaultName.Version < probedName.Version)
292295
{
293-
// With Aspire.TypeSystem's AssemblyVersion frozen at a build-decoupled constant,
294-
// the bundled and probed copies only differ in version when that constant was
295-
// bumped for a binary-incompatible contract change. Keep this as a warning: an old
296-
// bundle paired with libs built after such a bump is exactly the configuration that
297-
// fails to bind and silently skips integrations.
296+
// The bundled copy's strong-name version is strictly lower than the libs copy,
297+
// so it cannot satisfy the strong-named reference that the libs-side integration
298+
// assemblies carry to Aspire.TypeSystem. With the version frozen at a high
299+
// constant, this only happens for an already-shipped OLD CLI (a real, lower
300+
// version) paired with libs built after the freeze (referencing the constant) --
301+
// the unsupportable "update your CLI" case. The reference fails to bind and
302+
// integrations are silently skipped during type discovery, so surface it here.
298303
logger.LogWarning(
299-
"Shared assembly '{AssemblyName}' version mismatch: bundled={BundledVersion}, libs={LibsVersion} ({LibsPath}). " +
300-
"Integration assemblies referencing this assembly from the libs directory will fail to bind their type " +
301-
"references through the default load context, which causes integrations to be silently skipped during type discovery. " +
302-
"This typically indicates the apphost server bundle and the restored integration packages were produced by " +
303-
"different build configurations.",
304+
"Shared assembly '{AssemblyName}' version too low: bundled={BundledVersion}, libs={LibsVersion} ({LibsPath}). " +
305+
"The bundled copy is older than the integration packages and cannot satisfy their strong-named reference, " +
306+
"so integration assemblies will fail to bind their type references through the default load context and be " +
307+
"silently skipped during type discovery. The apphost server (CLI) is older than the restored Aspire packages; " +
308+
"update the Aspire CLI to a version at least as new as the packages.",
304309
sharedName,
305310
defaultName.Version,
306311
probedName.Version,
307312
libsPath);
308313
continue;
309314
}
310315

311-
// Same version, different MVID (compiled from different sources, e.g. a daily SDK
312-
// paired with a released CLI). This is expected and harmless: strong-name binding keys
313-
// on version + public key token, not MVID, so the bundled copy still satisfies the
314-
// reference. We can't read the probed MVID without loading the assembly, which we
315-
// deliberately don't do here. Logging the bundled MVID at Debug helps correlate with
316-
// any unrelated ReflectionTypeLoadException.
317-
logger.LogDebug("Shared assembly '{AssemblyName}' identity matches: Version={Version}, BundledMvid={Mvid}",
318-
sharedName, defaultName.Version, defaultMvid);
316+
// Bundled version >= libs version: the bundled copy satisfies the libs' strong-named
317+
// reference, so binding succeeds. This covers the supported "new CLI + older SDK"
318+
// backward-compat direction (bundled strictly higher) and the
319+
// same-version/differing-MVID pairing (e.g. a daily SDK paired with a released CLI) --
320+
// strong-name binding keys on version + public key token, not MVID. We can't read the
321+
// probed MVID without loading the assembly, which we deliberately don't do here.
322+
// Logging the bundled identity at Debug helps correlate with any unrelated
323+
// ReflectionTypeLoadException.
324+
logger.LogDebug("Shared assembly '{AssemblyName}' satisfies libs reference: bundled={BundledVersion}, libs={LibsVersion}, BundledMvid={Mvid}",
325+
sharedName, defaultName.Version, probedName.Version, defaultMvid);
319326
}
320327
}
321328

src/Aspire.Hosting.RemoteHost/IntegrationLoadContext.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,19 @@ internal IntegrationLoadContext(string[] probeDirectories, IntegrationPackagePro
5050
// AtsContext, etc.) work across the ALC boundary without requiring
5151
// reflection or marshalling.
5252
//
53-
// This binds Aspire.TypeSystem to the copy bundled in the apphost server
54-
// (the CLI's version), which can be OLDER than the SDK-restored codegen
55-
// assemblies loaded below (the supported "old CLI + new SDK" case). That is
56-
// only safe because Aspire.TypeSystem freezes its strong-name AssemblyVersion
57-
// to a constant decoupled from the build (see
58-
// src/Aspire.TypeSystem/Aspire.TypeSystem.csproj): as long as the shared contract
59-
// is unchanged, the older bundled copy satisfies the newer codegen assembly's
60-
// versioned reference regardless of CLI/SDK skew, so this short-circuit cannot
61-
// reintroduce the ReflectionTypeLoadException that silently dropped generators
62-
// (#18110, #17910). The version is bumped only on a binary-incompatible contract
63-
// change, which is exactly the case an older copy genuinely cannot satisfy.
53+
// This binds Aspire.TypeSystem to the copy bundled in the apphost server (the CLI's
54+
// version). That is safe because Aspire.TypeSystem freezes its strong-name
55+
// AssemblyVersion to a high constant (10001.0.0.0) decoupled from the build (see
56+
// src/Aspire.TypeSystem/Aspire.TypeSystem.csproj). The CLR satisfies a strong-named
57+
// reference when the loaded copy's version is >= the requested version, so the bundled
58+
// high-versioned copy satisfies the (lower, real-versioned) reference of any already
59+
// released SDK codegen assembly -- the supported "new CLI + older SDK" backward-compat
60+
// case -- and exactly matches any post-freeze codegen assembly (also 10001.0.0.0). This
61+
// avoids the ReflectionTypeLoadException that silently dropped generators (#18110,
62+
// #17910). The only case this short-circuit cannot satisfy is an already-shipped
63+
// OLD CLI (bundling a real, lower version) running post-freeze codegen that references
64+
// the high constant; that is the unsupportable "update your CLI" case surfaced
65+
// actionably by the #18125 diagnostics, not silently here.
6466
return null;
6567
}
6668

src/Aspire.TypeSystem/Aspire.TypeSystem.csproj

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,47 @@
1717
ILanguageSupport, AtsContext) keep a single type identity across the boundary
1818
(see src/Aspire.Hosting.RemoteHost/IntegrationLoadContext.cs and docs/specs/bundle.md).
1919
20-
The language codegen assemblies (Aspire.Hosting.CodeGeneration.*) are restored from the
21-
SDK side and can be NEWER than the bundled CLI copy (the supported "old CLI + new SDK"
22-
transition, bundle.md "Scenario 3"). These assemblies are strong-named (StrongNameKeyId=Open),
23-
so a build-floating AssemblyVersion (e.g. 13.5.0.0) would make the older bundled copy unable
24-
to satisfy a newer codegen assembly's strong-named reference. The reference fails to bind,
25-
Assembly.GetTypes() throws ReflectionTypeLoadException, and every generator is silently
26-
dropped -> "No code generator/language support found" (issues #18110, #17910).
27-
28-
The root problem is that AssemblyVersion moves on every build even when the contract is
29-
byte-for-byte unchanged (the 13.4 -> 13.5 regression changed nothing in this assembly except
30-
the strong-name version). So instead of tracking the product version, FREEZE AssemblyVersion
31-
at a constant that is decoupled from the build: any CLI/SDK combination binds regardless of
32-
minor/patch/MAJOR skew as long as the shared contract is unchanged.
33-
34-
Bump this constant ONLY when the shared contract changes in a binary-incompatible way (a member
35-
is removed or its signature changes). Such a change is exactly when an older bundled copy can no
36-
longer satisfy a newer reference, so a version bump is the correct signal for it; an additive
37-
change (new members only) keeps the same version because an older copy still satisfies callers.
38-
AtsSharedContractSurfaceTests is the back-compat guard: it fails on ANY surface change, forcing a
39-
deliberate decision about whether the change is additive (no bump) or breaking (bump this value).
40-
41-
Only AssemblyVersion is frozen; FileVersion, InformationalVersion, and the NuGet package version
42-
remain build-derived so diagnostics keep real build identities. Aspire.TypeSystem is consumed
43-
only via ProjectReference (no PackageReference consumers), so the frozen AssemblyVersion is not a
44-
package contract anyone binds to by exact version.
45-
46-
Freezing AssemblyVersion below the last-shipped value (13.4.0.0) intentionally violates package
47-
baseline validation's CP0003 rule, which requires a monotonically non-decreasing AssemblyVersion
48-
versus the PackageValidationBaselineVersion (src/Directory.Build.props). That rule exists to
49-
protect strong-name package consumers, but this package has none (ProjectReference only) and its
50-
strong-name version is now deliberately decoupled from the package version. So disable package
51-
baseline validation here, matching the sibling Aspire.Hosting.CodeGeneration.* packages in the
52-
same polyglot-codegen subsystem. The additive-vs-breaking contract is still enforced at the
53-
source level by AtsSharedContractSurfaceTests, so this does not weaken back-compat protection.
20+
The language codegen assemblies (Aspire.Hosting.CodeGeneration.*) are restored from the SDK
21+
side and reference this assembly by strong name (StrongNameKeyId=Open). The CLR satisfies a
22+
strong-named reference only when the loaded (here: bundled) copy's AssemblyVersion is GREATER
23+
THAN OR EQUAL TO the requested version. If AssemblyVersion floated with the build, the version
24+
on either side would diverge whenever the CLI and SDK were built at different times, the
25+
reference would fail to bind, Assembly.GetTypes() would throw ReflectionTypeLoadException, and
26+
every generator would be silently dropped -> "No code generator/language support found"
27+
(issues #18110, #17910). A floating version cannot satisfy BOTH skew directions at once: it
28+
necessarily breaks either "new CLI + older Aspire.Hosting" (backward compat, bundle.md
29+
"Scenario 2") or "old CLI + newer Aspire.Hosting".
30+
31+
Fix: FREEZE AssemblyVersion at a high constant decoupled from the build. The value must sit
32+
ABOVE every already-shipped real version so the bundled copy satisfies the (lower, real)
33+
reference of any already-released codegen assembly -> the common, must-work backward-compat
34+
direction (a current CLI running an existing app) binds. Going forward, every CLI and SDK
35+
built after this freeze carries the same constant, so they match exactly and no skew matters.
36+
10001.0.0.0 is chosen as a "1.0" seeded high enough to clear all real product versions for
37+
the product's lifetime (AssemblyVersion components are capped at 65535); the major slot is
38+
the bump axis (10001 -> 10002) for the rare breaking change below.
39+
40+
The only residual hard-bind failure is an already-shipped OLD CLI (bundling a real, lower
41+
version) paired with post-freeze codegen referencing this constant. That is the genuinely
42+
unsupportable "the CLI predates this build" case: it cannot be fixed for an immutable shipped
43+
CLI regardless of the freeze value, and the user must update the CLI. The #18125 diagnostics
44+
turn that into an actionable "run aspire update" message. (Post-freeze CLIs never hit it: they
45+
all bundle this constant and satisfy any post-freeze reference.)
46+
47+
Bump this constant ONLY when the shared contract changes in a binary-incompatible way (a
48+
member is removed or its signature changes); an additive change (new members only) keeps the
49+
same value. AtsSharedContractSurfaceTests is the back-compat guard: it fails on ANY surface
50+
change, forcing a deliberate decision about whether the change is additive (no bump) or
51+
breaking (bump this value).
52+
53+
Only AssemblyVersion is frozen; FileVersion, InformationalVersion, and the NuGet package
54+
version remain build-derived so diagnostics keep real build identities. Aspire.TypeSystem is
55+
consumed only via ProjectReference (no PackageReference consumers), so the frozen
56+
AssemblyVersion is not a package contract anyone binds to by exact version. Because
57+
10001.0.0.0 is >= the package baseline version, package baseline validation (CP0003) still
58+
passes, so no validation override is required.
5459
-->
55-
<AssemblyVersion>1.0.0.0</AssemblyVersion>
56-
<DisablePackageBaselineValidation>true</DisablePackageBaselineValidation>
60+
<AssemblyVersion>10001.0.0.0</AssemblyVersion>
5761
</PropertyGroup>
5862

5963
</Project>

0 commit comments

Comments
 (0)