This technique is designed specifically for documentation generation and analysis tools. It should not be used in production code to bypass normal
encapsulation principles.
The Internal Documentation Problem
It used to be that great documentation was only really needed for Component Libraries. For 25 years, internal teams got the short shrift when it came to docs. So when looking at everything we wanted in a modern documentation system, we wanted it to build beautiful content for internal teams as well. We’d seen too many organizations struggle with incomplete documentation that left developers guessing about implementation details. The promise was simple: create comprehensive documentation that would make LLMs incredibly effective at helping teams write code against their internal systems. But we quickly hit a wall. Consider this typical internal library structure:ProcessPaymentAsync
method. But developers working with this system need to
understand:
- How validation works internally
- What helper classes are available
- The patterns used for error handling
- The complete flow of payment processing
The Traditional Solution: InternalsVisibleTo
.NET providesInternalsVisibleTo
for scenarios where you need to expose internal members:
AssemblyInfo.cs
just to generate better documentation.
Plus, InternalsVisibleTo
requires the source assembly to be recompiled with knowledge of the consuming assembly. That’s exactly the opposite of what we
needed - we wanted to analyze existing assemblies without any modifications.
Enter the Strathweb Solution
Our breakthrough came from an ingenious blog post by Filip W on bypassing C# visibility rules with Roslyn. Filip discovered that the .NET runtime includes a little-known attribute calledIgnoresAccessChecksToAttribute
that can bypass visibility checks entirely.
Here’s the key insight: while InternalsVisibleTo
works at compile-time and requires the source assembly to “opt-in” to visibility,
IgnoresAccessChecksToAttribute
works at the consumer side - the assembly that wants to see internals declares that it should ignore access checks for a
specific target assembly.
Filip’s original example showed how to access internal members of a third-party library:
Our Innovation: Dynamic Bridge Assemblies
The breakthrough was realizing we could create these “bridge” assemblies dynamically using Roslyn. Instead of pre-compiling an assembly withIgnoresAccessChecksTo
, we generate the bridge assembly on-the-fly for each target assembly we want to document.
Here’s how our CreateCompilationAsync
method works:
Why Dynamic Generation?
You might wonder: “Why not just addIgnoresAccessChecksToAttribute
directly to DotNetDocs.Core?” The answer reveals a fundamental limitation of how .NET
assembly loading works:
-
Assembly Identity Matters: The
IgnoresAccessChecksTo
attribute must be applied to the assembly that’s requesting access. We can’t pre-compile this into DotNetDocs.Core because we don’t know what assemblies we’ll be analyzing. - Runtime vs. Compile-time: The attribute needs to be present when the Roslyn compilation analyzes the target assembly. By generating it dynamically, we create a fresh “bridge” compilation for each target.
-
Framework Compatibility: The
IgnoresAccessChecksToAttribute
isn’t available in all .NET versions. By generating it ourselves, we ensure compatibility across frameworks. -
Symbol Resolution: The combination of
MetadataImportOptions.All
and the bridge assembly tells Roslyn’s symbol resolution engine to treat internal members as accessible.
The Magic Behind the Scenes
1
Target Assembly Analysis
DotNetDocs loads the target assembly and extracts its name for the bridge compilation.
2
Attribute Generation
We dynamically generate the
IgnoresAccessChecksToAttribute
since it’s not universally available.3
Bridge Compilation
A new Roslyn compilation is created that applies the attribute to ignore access checks for the specific target assembly.
4
Enhanced Symbol Access
With
MetadataImportOptions.All
and the bridge attribute, Roslyn’s symbol API now treats internal members as accessible.5
Documentation Generation
We can now traverse and document the complete API surface, including internal implementations.
Real-World Impact for LLM-Assisted Development
This technique transforms what’s possible with LLM-assisted development for internal teams. Consider how much more helpful an AI coding assistant becomes when it has access to complete API documentation:Internal APIs
Complete method signatures, parameter details, and usage patterns for internal helper classes
Helper Classes
Utility classes and their methods that teams can leverage in their own code
Technical Deep Dive
Performance Considerations
Compilation Caching
Bridge compilations are cached based on the target assembly’s last modified time and the set of included members. This means we only regenerate the bridge when the target assembly actually changes, making incremental builds very fast.
Memory Management
The
AssemblyManager
implements IDisposable
to properly clean up the Roslyn compilation and associated memory. This is crucial when processing multiple assemblies in batch operations.Security and Boundaries
Documentation-Only Access
The bridge assembly technique only works during documentation generation. It doesn’t create runtime access to internal members - the generated documentation simply describes what’s available.
Respecting Intent
Our documentation clearly marks internal APIs as such, warning developers that these are implementation details that may change. We’re not encouraging misuse of internal APIs, just providing complete context.
Best Practices for Internal Teams
When using DotNetDocs with bridge assemblies for internal documentation:- Use comprehensive XML documentation on internal members
- Structure internal APIs with the same care as public ones
- Include examples in internal API documentation
- Consider internal APIs as part of your team’s knowledge base
- Use the generated docs to onboard new team members
Looking Forward: The LLM Documentation Revolution
As LLMs become increasingly central to software development, the quality and completeness of our documentation becomes a competitive advantage. Teams with comprehensive internal documentation can leverage AI assistants much more effectively than those with incomplete docs. Bridge assemblies represent just one piece of this puzzle, but it’s a crucial one. By giving LLMs complete context about how our systems work internally, we enable them to provide much more valuable assistance.This is why we built DotNetDocs specifically for internal teams. Public API documentation is a solved problem - but helping teams document and leverage their
internal systems? That’s where the real productivity gains live.
Conclusion
The bridge assembly technique started as a solution to a technical problem: how do you document internal APIs when traditional tools can’t see them? But it’s evolved into something much more significant - a key enabler for LLM-assisted development in enterprise environments. By dynamically generatingIgnoresAccessChecksTo
attributes through Roslyn compilations, we’ve unlocked the ability to create comprehensive documentation
that tells the complete story of how internal systems work. This isn’t just about better docs - it’s about making AI coding assistants exponentially more
helpful for internal development teams.
The next time you’re working with an AI coding assistant and wish it understood your internal systems better, remember: the quality of the assistant’s help
is directly related to the quality of the documentation it can access. Bridge assemblies help ensure that documentation tells the whole story.
Want to see this technique in action? Check out our AssemblyManager implementation for the complete source code,
or try DotNetDocs on your own internal libraries.
This technique builds on Filip W’s excellent research on bypassing C# visibility rules with Roslyn. We’ve adapted and extended his approach specifically for dynamic documentation generation scenarios.