How did Microsoft create assemblies that have circular references?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In ordinary .NET development, circular project references are not allowed. So when developers notice framework assemblies that appear to reference each other, the natural question is how Microsoft built them. The short answer is that they generally did not solve this with normal C# project references. They used lower-level build techniques such as reference assemblies, staged compilation, facades, and type forwarding.
Source-Level Cycles Versus Metadata-Level Cycles
A Visual Studio solution usually rejects this kind of structure:
- project
Areferences projectB - project
Breferences projectA
That is a source build problem because the compiler and build graph need an order. But the CLR metadata model is more flexible. An assembly can contain references to types defined elsewhere, and those references may be resolved later at runtime.
So there are two separate questions:
- can two source projects compile directly against each other in one simple pass
- can two finished assemblies participate in a graph that looks cyclic
The first is usually no. The second can be yes.
Reference Assemblies Break the Build-Time Cycle
One common technique is to compile against a contract rather than the final implementation assembly. A reference assembly contains only the public surface needed for compilation.
That allows a staged build:
- build reference contracts
- compile implementations against those contracts
- ship implementation assemblies that satisfy the references
The build no longer requires project A and project B to compile against each other's implementation at the same time.
Type Forwarding and Facades
Another important tool is type forwarding. A facade assembly can declare that a type logically exposed from one assembly is actually implemented elsewhere.
A simplified example looks like this:
This does not create the implementation. It redirects type identity so the runtime and compiler can resolve APIs without every assembly physically defining everything itself.
Framework codebases use facades and forwarders heavily to separate:
- API surface
- implementation location
- compatibility layers across target frameworks
That can make the final assembly graph look more intertwined than the original source projects.
Why Microsoft Could Use Tools You Normally Do Not
The .NET team controls the build pipeline, metadata emission, and packaging process. That means they can do multi-stage builds with tooling below the level of ordinary project references. They are not limited to "create two C# class library projects and hit build."
For example, a sophisticated pipeline can:
- emit metadata first
- compile against reference artifacts
- replace or supplement assemblies later in the pipeline
- add forwarding or facade assemblies during packaging
That is very different from a small application solution where the build graph must be straightforward.
What This Means for Regular Application Code
If you hit a circular reference in your own solution, the right fix is usually architectural, not a trick. Common approaches include:
- extract shared interfaces into a third assembly
- move common DTOs or abstractions into a contracts assembly
- invert dependencies with dependency injection
Example:
Both higher-level assemblies can reference a shared contracts assembly that contains IMessageBus, removing the direct cycle.
The Key Idea
When people say Microsoft has assemblies with circular references, they are usually observing the packaged runtime or framework graph, not a simple source-level project cycle. Those are different layers of the system.
The practical lesson is:
- build-time cycles are usually a design problem
- runtime assembly relationships can be shaped with contracts, facades, and forwarding
Common Pitfalls
- Assuming the C# project system and CLR metadata rules are the same thing.
- Trying to reproduce framework packaging tricks in a normal application solution.
- Treating an apparent runtime assembly cycle as proof that direct project cycles should be acceptable.
- Ignoring simpler fixes such as extracting shared abstractions into a third assembly.
- Confusing type forwarding with actual implementation sharing.
Summary
- Normal .NET project references do not support direct circular build dependencies.
- Framework assemblies that appear cyclic are usually built with staged techniques such as reference assemblies and type forwarding.
- Facade assemblies separate API surface from implementation location.
- Microsoft can use lower-level build and packaging steps that most application projects do not need.
- In ordinary codebases, breaking the cycle with contracts or abstractions is usually the correct solution.

