UPDATE: ok, as you found, the circular reference was the wrong guess, as there are other situation that may cause similar behavior.
The more general way to describe the problem is that batching at runtime works in a very permissive way which can mask problems. Basically, we try to batch everything in one folder, but if we get a compile error when compiling that batch, we fall back to individual file compilation. In many cases that works fine, but sometimes, this can lead to a given page getting compiled twice (similar to what I described below, but for a different reason).
On the other hand, aspnet_compiler works in a strict way, where if batching fails it fails altogether and does not fall back. That's why running this tool is a great way to locate various type of issues (or latent issues) that can be far from obvious at runtime. I guess we didn't do a good job evangelizing this tool for this purpose :)
As for why renaming the file fixed it, this may be caused by it changing the ordering in which files are processed, which is a bit arbitrary. It may be that if you rename it to something else, you'll see it happen again.
Frankly, looking back I kind of wish we had made this batching behavior strict at runtime, to catch those situations earlier. The reason we chose the current fall back design was to avoid failing whenever possible, but this came with a price: when something is wrong, it's a pain to catch it :)
ORIGINAL ANSWER:
In short, the problem is that when batching is turned on (and it is by default), you should avoid having directory level circular dependencies. Let me explain what I mean by that.
Here is an example. Say you have:
- In folder1: page.aspx and uc2.ascx
- In forder2: uc1.ascx
And say that page.aspx references uc1.ascx (via a @register directive), and that uc1.ascx references uc2.ascx. At the file level, that’s perfectly fine, but at the directory level, there is a circular dependency: folder1 references something in folder2, which references something in folder1.
Why this is problematic has to do with how batching works: when you request the page, it first tries to compile everything in folder1 together. But since folder1/page.aspx references folder2/uc1.ascx, it needs to compile folder2 before it can do folder1. But then uc1 uses uc2, meaning it must first do folder1! At this point, ASP.NET detects the situation and tries to make the best of it by compiling uc2.asc by itself. While this allows some scenarios to work, it can also cause weird things because some items end up compiled in two assemblies. Here, uc2.ascx would be both compiled by itself and with the folder1 batch.
There is actually a way to easily detect if your site has such folder level circular dependencies. From a VS console window, go to the root of your site and run:
aspnet_compiler -v foo -p .
If you have folder level circular dependencies, you’ll get some errors that look like:
/foo/Sub/UC1.ascx(2): error ASPPARSE: Circular file references are not allowed.
The cheap way to avoid this issue is what you already know: disable batching. Now at least you know why that works :)
But the better thing to do if you can is to avoid the folder level circular dependencies. If you start thinking of each folders as a ‘component’ which produces an assembly, that actually makes sense, and can help make the pieces of your site more modular.
Yes, it’s also fair to call this a ‘bug’ in the compilation system, or at least a limitation. But once you’re aware of it, it’s fairly easy to avoid.