Skip to main content
Picture this: You’re part of an internal development team that’s built an amazing library with thoughtful internal APIs, well-structured helper classes, and carefully designed implementation patterns. Your team wants to leverage AI coding assistants like GitHub Copilot or Claude to help write better code against your internal systems. But there’s a problem. LLMs can only help you with what they can see. And traditional documentation tools? They can only document public APIs, leaving a massive blind spot in your internal architecture. This is the story of how we solved that problem with a technique we call “bridge assemblies” - and why it matters more now than ever in the age of LLM-assisted development.
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:
Traditional documentation tools would generate docs showing only the public 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
Without this context, LLMs can only provide generic suggestions instead of system-specific guidance.

The Traditional Solution: InternalsVisibleTo

.NET provides InternalsVisibleTo for scenarios where you need to expose internal members:
This approach works great when you know the target assembly name at compile time. The problem? DotNetDocs runs as a post-build tool, analyzing already-compiled assemblies. We can’t predict what our tool will be called, and we certainly can’t ask every team to modify their 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 called IgnoresAccessChecksToAttribute 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:
But Filip’s approach still required knowing the target assembly at compile time. We needed something more dynamic.

Our Innovation: Dynamic Bridge Assemblies

The breakthrough was realizing we could create these “bridge” assemblies dynamically using Roslyn. Instead of pre-compiling an assembly with IgnoresAccessChecksTo, 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 add IgnoresAccessChecksToAttribute directly to DotNetDocs.Core?” The answer reveals a fundamental limitation of how .NET assembly loading works:
  1. 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.
  2. 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.
  3. Framework Compatibility: The IgnoresAccessChecksToAttribute isn’t available in all .NET versions. By generating it ourselves, we ensure compatibility across frameworks.
  4. 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:
The documentation generated with bridge assemblies includes:

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:

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 generating IgnoresAccessChecksTo 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.