Skip to content

Include requesting assembly chain in assembly load failure exceptions#125795

Open
Copilot wants to merge 34 commits intomainfrom
copilot/fix-issue-9185
Open

Include requesting assembly chain in assembly load failure exceptions#125795
Copilot wants to merge 34 commits intomainfrom
copilot/fix-issue-9185

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 19, 2026

This pull request enhances .NET exception diagnostics for assembly loading failures by tracking and surfacing the chain of requesting assemblies when a dependency is missing or invalid. This helps developers more easily identify which assemblies triggered a load failure, especially in complex dependency scenarios.

Exception information:

  • The constructors for BadImageFormatException, FileLoadException, and FileNotFoundException now accept an additional requestingAssemblyChain parameter, which records the chain of assemblies that led to the load failure. This chain is stored and included in the exception’s string representation.
  • The ToString() methods of these exceptions now append a "Requested by: ..." message listing the requesting assembly chain, improving error clarity for users.
  • A new resource string, IO_FileLoad_RequestedBy, is added for this message.

Runtime and binding cache:

  • The runtime now allows querying the parent (requesting) assembly for an assembly in the AssemblySpecBindingCache. New methods allow walking the chain of requesting assemblies up to a depth of 10, building a diagnostic chain for exceptions.
  • The EEFileLoadException and related logic are updated to capture and propagate the requesting assembly chain, including in exception cloning and when thrown from the runtime.
  • The unmanaged-to-managed exception creation path is updated to pass the requesting assembly chain to managed exception constructors.
  • The chain is a best-effort representation. An assembly can be requested by multiple different parents. The message uses the parent of the first matching cached assembly found during iteration.

Testing and validation:

  • Adds new test assemblies (MissingDependency.Leaf, MissingDependency.Mid) and a test in AssemblyLoadContextTest to verify that the requesting assembly chain appears in exception messages when a transitive dependency is missing.

These changes significantly improve the developer experience when diagnosing assembly loading failures by providing more actionable information in exception messages.

Example:

System.IO.FileNotFoundException: Could not load file or assembly 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
File name: 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
Requested by: LibB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> LibA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> helloworld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
 --> System.Private.CoreLib, Version=11.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
   at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5
   at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5
   at LibA.LibAClass.GetMessage() in C:\repos\helloworld\LibA\Class1.cs:line 5
   at Program.Main(String[] args) in C:\repos\helloworld\Program.cs:line 329

@dotnet/appmodel @AaronRobinsonMSFT


💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Copilot AI and others added 2 commits March 19, 2026 19:02
When an assembly fails to load, the FileLoadException/FileNotFoundException
now includes the name of the requesting (parent) assembly in the FusionLog
property. This helps diagnose dependency chain issues by showing which
assembly triggered the failed load.

The information flows from the native AssemblySpec::GetParentAssembly()
through EEFileLoadException::CreateThrowable() to the managed exception's
FusionLog property, which is displayed in ToString() output.

Fixes #9185

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
BadImageFormatException is also created by EEFileLoadException::CreateThrowable()
when the HRESULT maps to kBadImageFormatException. Without the 3-arg constructor,
the requesting assembly info would be silently dropped for bad image format errors.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
@elinor-fung
Copy link
Copy Markdown
Member

@copilot we should include the full requesting assembly chain instead of just the parent.

Walk the inner exception chain to build the full dependency path.
When A→B→C→D and D fails, D's FusionLog now shows:

  Requesting assembly: 'C'
  Requesting assembly: 'B'
  Requesting assembly: 'A'

At each Throw(AssemblySpec*) level, the current parent assembly name
is appended to every inner EEFileLoadException's chain. The managed
FormatRequestingAssemblyChain helper formats each newline-separated
name with the "Requesting assembly:" resource string.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 19, 2026

@copilot we should include the full requesting assembly chain instead of just the parent.

Updated in 228155e. Now at each Throw(AssemblySpec*) level, the current parent assembly name is appended to every inner EEFileLoadException's chain. For a dependency chain A→B→C→D where D is missing, D's FusionLog will show:

Requesting assembly: 'C, Version=...'
Requesting assembly: 'B, Version=...'
Requesting assembly: 'A, Version=...'

The managed FormatRequestingAssemblyChain helper (shared by all three exception types) formats each newline-separated name from the native chain string.

Copilot AI changed the title Include requesting assembly in assembly load failure exceptions Include full requesting assembly chain in assembly load failure exceptions Mar 19, 2026
…dd tests

The requesting assembly chain is now appended to the exception's Message
property instead of FusionLog. This means users see the chain directly
in the exception message when it is thrown, without needing to inspect
FusionLog separately.

Added tests for all three exception types (FileLoadException,
FileNotFoundException, BadImageFormatException) that verify:
- Single requesting assembly appears in Message
- Multiple requesting assemblies (chain) appear in Message
- FusionLog remains null

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
…missing transitive dependency

Remove the 6 reflection-based tests that invoked private constructors via
reflection from BadImageFormatExceptionTests, FileLoadExceptionTests, and
FileNotFoundExceptionTests.

Add an integration test in System.Runtime.Loader.Tests that creates a real
dependency chain (Root -> Mid -> Leaf) where the transitive dependency
(Leaf) is missing at runtime. The test verifies that a FileNotFoundException
is thrown with the correct missing assembly name when the chain is exercised.

Three helper assembly projects are added:
- MissingDependency.Leaf (not deployed via PrivateAssets=all)
- MissingDependency.Mid (depends on Leaf)
- MissingDependency.Root (depends on Mid)

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
The test now asserts that the FileNotFoundException message contains
"MissingDependency.Mid" (the requesting assembly), verifying that the
entire dependency chain is reported in the exception message, not just
the missing assembly file name.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
The test now asserts that both MissingDependency.Mid and
MissingDependency.Root appear in the exception message, verifying
the entire dependency chain (Root → Mid → Leaf) is reported.

Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 6, 2026 17:43
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 21 out of 21 changed files in this pull request and generated 1 comment.

Comment on lines 70 to +83
@@ -77,6 +78,9 @@ public override string ToString()
if (!string.IsNullOrEmpty(FileName))
s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, FileName);

if (!string.IsNullOrEmpty(_requestingAssemblyChain))
s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings());

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The new _requestingAssemblyChain field is appended in ToString(), but it isn’t preserved across exception serialization/deserialization (the SerializationInfo ctor and GetObjectData only round-trip FileName/FusionLog). Since this type is still [Serializable] and already maintains custom serialization fields, please include the requesting assembly chain in the serialization payload and restore it in the serialization ctor so diagnostics survive remoting/logging scenarios that serialize exceptions.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@jkotas
Copy link
Copy Markdown
Member

jkotas commented Apr 7, 2026

Serialization — Intentionally not updated
The [Obsolete] GetObjectData methods do not serialize _requestingAssemblyChain. This is the correct decision — BinaryFormatter serialization is deprecated and will be removed. Adding new fields to deprecated serialization paths would be inappropriate. (Previous Copilot reviews incorrectly flagged this as a bug.)

This contradicts what we have just done in #126428 . We should be consistent - either keep updating the binary serializers or not at all.

@github-actions

This comment has been minimized.

Restore the requesting assembly chain across legacy exception
serialization round-trips for FileLoadException,
FileNotFoundException, and BadImageFormatException, keeping the
behavior consistent with other updated binary serializer paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 7, 2026 17:07
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 21 out of 21 changed files in this pull request and generated 4 comments.

{
FileName = info.GetString("FileNotFound_FileName");
FusionLog = info.GetString("FileNotFound_FusionLog");
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The serialization constructor now unconditionally reads "FileNotFound_RequestingAssemblyChain" via SerializationInfo.GetString. If this exception is deserialized from older serialized data that doesn't contain this entry, GetString will throw a SerializationException. Consider treating this value as optional (e.g., catch SerializationException / check for the entry) and default to null when missing for better forward/backward compatibility.

Suggested change
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
try
{
_requestingAssemblyChain = info.GetString("FileNotFound_RequestingAssemblyChain");
}
catch (SerializationException)
{
_requestingAssemblyChain = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 84
protected FileLoadException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
FileName = info.GetString("FileLoad_FileName");
FusionLog = info.GetString("FileLoad_FusionLog");
_requestingAssemblyChain = info.GetString("FileLoad_RequestingAssemblyChain");
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The serialization constructor now unconditionally reads "FileLoad_RequestingAssemblyChain" via SerializationInfo.GetString. If FileLoadException is deserialized from older serialized data that lacks this entry, GetString will throw. Consider making this field optional during deserialization (default to null when missing).

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

Use SerializationInfo.GetValueNoThrow for the new
*_RequestingAssemblyChain fields so older payloads that do not
contain the data still deserialize successfully.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

…est.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 7, 2026 18:01
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 21 out of 21 changed files in this pull request and generated 2 comments.

@github-actions

This comment has been minimized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

🤖 Copilot Code Review — PR #125795

Note

This review was generated by GitHub Copilot using Claude Opus 4.6 and Claude Sonnet 4 models.

Holistic Assessment

Motivation: The PR adds a requesting assembly chain to file load exceptions (FileNotFoundException, FileLoadException, BadImageFormatException) so users can diagnose transitive dependency loading issues. This addresses a real diagnostic gap — previously, when an assembly failed to load, there was no indication of which assemblies requested it. The most recent commits fix robustness issues: guarding GetDisplayName against exceptions, adding serialization round-trip support, and fixing a malformed XML tag.

Approach: The approach is sound — the chain is built in native code (walking the binding cache), passed through the UnmanagedCallersOnly bridge into managed exception constructors, and surfaced in ToString(). Using EX_TRY to guard the entire chain-building operation (including GetDisplayName) is the correct defensive strategy since this is best-effort diagnostic data that must not disrupt the primary exception.

Summary: ✅ LGTM. The changes are correct, well-structured, and follow established patterns. The contract relaxation is justified, the serialization uses the correct backward-compatible approach, and the test coverage validates the end-to-end scenario. Minor observations below are non-blocking.


Detailed Findings

✅ Correctness — Moving GetDisplayName and SetRequestingAssemblyChain inside EX_TRY (clrex.cpp)

This is the core fix. Previously, pParentAssembly->GetDisplayName(requestingChain) was outside the EX_TRY block — if GetDisplayName threw (e.g., assembly in a bad state), the exception would propagate and disrupt the primary EEFileLoadException being thrown. Moving it inside EX_TRY ensures the primary diagnostic exception is preserved.

Moving SetRequestingAssemblyChain inside EX_TRY means if the chain walk partially succeeds but then fails, no partial data is set. This is actually safer than the previous behavior: if SString::Append throws mid-operation, the chain string could be in an inconsistent state, and the old code would have set that corrupt partial data. The all-or-nothing approach is the right tradeoff for best-effort diagnostic data.

✅ Correctness — Serialization with GetValueNoThrow (BadImageFormatException.cs, FileLoadException.cs, FileNotFoundException.cs)

Using GetValueNoThrow for deserialization of _requestingAssemblyChain is the established pattern in CoreLib for optional fields that may not be present in older serialized payloads. This correctly matches the pattern used by Exception._data, TimeZoneInfo.AdjustmentRule, and SecurityException. The symmetry of AddValue in GetObjectData is correct.

✅ Correctness — XML fix in Strings.resx

The fix from <data/> to </data> corrects a malformed XML closing tag introduced in an earlier commit of this PR. <data/> is a self-closing empty element, not a proper closing tag for the <data name="IO_FileLoad_RequestedBy"> element. This fix is necessary for the resx to be valid XML.

💡 Observation — Contract relaxation in appdomain.cpp

The contract changed from explicit THROWS; GC_NOTRIGGER; MODE_ANY; to STANDARD_VM_CONTRACT (which expands to just THROWS;). I verified the internal calls:

  • GetParentAssemblyMap: contract is THROWS; GC_NOTRIGGER; MODE_ANY;
  • PEAssembly::GetDisplayName (via Assembly::GetDisplayName): contract is THROWS; GC_NOTRIGGER; MODE_ANY;
  • SString::Append: native heap operations, no GC

All internal operations are GC_NOTRIGGER, so STANDARD_VM_CONTRACT (which doesn't restrict GC) is technically a relaxation — the old contract was more precise. This isn't wrong (the method will still not trigger GC in practice), but the old explicit contract was more self-documenting about the GC invariant, especially since DomainCacheCrstHolderForGCPreemp is designed for preemptive mode. That said, STANDARD_VM_CONTRACT is the common idiom throughout the VM codebase, so this is consistent with local style. Non-blocking.

✅ Test — AssemblyLoadContextTest.cs assertions

The test correctly validates the end-to-end scenario: Root → Mid → Leaf where Leaf is missing. The assertions check:

  • ex.FileName contains "MissingDependency.Leaf" (the missing assembly)
  • ex.ToString() contains "MissingDependency.Mid" (the immediate requester)
  • ex.ToString() contains " --> " (the chain separator)
  • ex.ToString() contains "MissingDependency.Root" (the transitive requester)

The [MethodImpl(MethodImplOptions.NoInlining)] on MidClass.UseLeaf() and RootClass.UseMiddle() correctly prevents the JIT from inlining, which could otherwise alter the requesting assembly chain.

✅ Architecture — Test helper projects

The MissingDependency.{Leaf,Mid,Root} test projects are well-structured. The use of <PrivateAssets>all</PrivateAssets> on Mid's reference to Leaf correctly prevents Leaf.dll from being deployed to the test output, simulating a real-world missing transitive dependency scenario.

✅ Verified — CloneHelper preserves chain data

EEFileLoadException::CloneHelper() correctly copies m_requestingAssemblyChain to the clone via SetRequestingAssemblyChain, ensuring the chain survives exception cloning.


Models: Claude Opus 4.6 (primary), Claude Sonnet 4 (cross-validation). GPT-5.4 timed out after 10 minutes and did not contribute to this review.

Generated by Code Review for issue #125795 ·

@elinor-fung
Copy link
Copy Markdown
Member

/azp run

@azure-pipelines
Copy link
Copy Markdown

You have several pipelines (over 10) configured to build pull requests in this repository. Specify which pipelines you would like to run by using /azp run [pipelines] command. You can specify multiple pipelines using a comma separated list.

@elinor-fung
Copy link
Copy Markdown
Member

/azp run runtime

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants