Why are the temporary UserCache XMLs being reused when my application restarts?

Topics: ASP.NET MVC
Nov 16, 2012 at 7:04 PM

a couple of XML files are created in the following location when an MVC application starts.

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\<AppName>\Sub1\Sub1\UserCache

MVC-AreaRegistrationTypeCache.xml and MVC-ControllerTypeCache.xml

When I change or just re-save my web.config file, the app restarts, and MVC reuses those XML files from a previous run.  For simple changes, and especially if no new assemblies are added, then this doesn't cause any problems.

I have found that if I re-save global.asax, then these xml files do get regenerated.  This is my current solution for my following problem.

In my case, I change the web.config to point to a plugin assembly that contains controllers and areas.  The application restarts, my app finds the new assembly, I do a BuildManager.AddReferencedAssembly(assembly), but MVC doesn't care, because it reuses the cached area assemblies in those XML files.  The restart, in my opinion, is corrupt because you are using files on disk from a previous run.  

If I go and delete those files by hand, then MVC doesn't find them, and things are good.   Also, as stated above, when I resave global.asax this seems to also recreate the XML files, which is good too, just not desirable from a deployment point of view.

I view this as a defect due to corruption criterias.

I did look at the MVC source, and I couldn't find any way to turn this off, as it is hard coded in AreaRegistration.cs to use the cache if it exists.

I haven't tracked down why re-saving global.asax results in a complete refresh, vs web.config.

So, will this become a defect that will be corrected, or is there another solution that I don't know about yet that doesn't make me go down the global.asax route.

Thanks

Coordinator
Nov 20, 2012 at 7:18 PM

Hi there,

This does sound like it might be a bug. The cache is an optimization that saves the list of controller types when an app restarts so that MVC doesn't have to scan every assembly every time the app starts. For large applications this can even take over 1 minute.

The reason that re-saving global.asax fixes it is that it causes the whole app to be recompiled, so all the old caches are thrown away (or a whole new cache folder is used). There are other operations that will cause the cache to be reset as well. There is no way to configure this in MVC aside perhaps from plugging in some custom assembly resolvers/locators.

Can you clarify when is your app calling BuildManager.AddReferencedAssembly? Is it from Application_Start, PreAppStart, or some other point in time?

If this ends up being a bug in MVC then we'll get a bug logged in the issue tracker. It could be that MVC is missing a check for dynamically referenced assemblies - I think that feature was added to ASP.NET after MVC was already released, so maybe MVC has to "catch up" with the latest ASP.NET features.

Thanks,

Eilon

Nov 20, 2012 at 7:49 PM
Thanks for the reply.
I don't have the code in front of me so forgive my namings. I reference the assemblies as a preappinit step, so not as part of the mvc pipeline. Well before App_Start.

Please be aware that it is controllers I am adding and removing so you may be caching an assembly that I am not loading any longer.


If caching is worthy of implementation then let's think about abstracting it away from mvc core functionality. In my case, I don't want it at all because I can live with the consequences. If others think it is a show stopper issue then they can implement it and mvc core makes a callout for the cached assemblies. You could also implement the xml version on your end as long as I can turn it completely off.

Thanks
Herb
Sent from my Verizon Wireless BlackBerry

From: "eilonlipton" <notifications@codeplex.com>
Date: 20 Nov 2012 11:18:27 -0800
To: <ghstahl@gmail.com>
ReplyTo: ASPNETWebStack@discussions.codeplex.com
Subject: Re: Why are the temporary UserCache XMLs being reused when my application restarts? [ASPNETWebStack:403529]

From: eilonlipton

Hi there,

This does sound like it might be a bug. The cache is an optimization that saves the list of controller types when an app restarts so that MVC doesn't have to scan every assembly every time the app starts. For large applications this can even take over 1 minute.

The reason that re-saving global.asax fixes it is that it causes the whole app to be recompiled, so all the old caches are thrown away (or a whole new cache folder is used). There are other operations that will cause the cache to be reset as well. There is no way to configure this in MVC aside perhaps from plugging in some custom assembly resolvers/locators.

Can you clarify when is your app calling BuildManager.AddReferencedAssembly? Is it from Application_Start, PreAppStart, or some other point in time?

If this ends up being a bug in MVC then we'll get a bug logged in the issue tracker. It could be that MVC is missing a check for dynamically referenced assemblies - I think that feature was added to ASP.NET after MVC was already released, so maybe MVC has to "catch up" with the latest ASP.NET features.

Thanks,

Eilon

Coordinator
Nov 25, 2012 at 11:42 PM

I believe the bug is somewhere in the "guts" of MVC's DefaultControllerFactory. While some of the code in there is public, some of it is unfortunately private and internal. But you could probably derive from DefaultControllerFactory and implement the GetControllerType yourself that does no caching at all. You'd just look at all the loaded assemblies and find the type you want. I'm sure you could (and probably should) copy lots of bits and pieces from the existing MVC code so you can preserve logic such as the "Controller" suffix and whatnot.

Nov 26, 2012 at 7:40 PM
I haven't look at that, but I would think the damage is already done before I can utilize this.
The first place this fails is when calling AreaRegistration.RegisterAllAreas(); from within App_Start. I had previously called BuildManager.AddReferencedAssembly(assembly); via a [assembly: PreApplicationStartMethod(typeof (PreApplicationInit), "Initialize")] technique. The assembly I registered has my controllers that are in an area. So even though my assembly is there, it is not being registered because of the caching, hence there is nothing for to help with my controller factory. As far as MVC is concerned, if I am not registered I don't exist.
On another note, did this make it into a defect? What is the release cycle that happens when you have defect releases.
Thanks
Herb


On Sun, Nov 25, 2012 at 3:42 PM, eilonlipton <notifications@codeplex.com> wrote:

From: eilonlipton

I believe the bug is somewhere in the "guts" of MVC's DefaultControllerFactory. While some of the code in there is public, some of it is unfortunately private and internal. But you could probably derive from DefaultControllerFactory and implement the GetControllerType yourself that does no caching at all. You'd just look at all the loaded assemblies and find the type you want. I'm sure you could (and probably should) copy lots of bits and pieces from the existing MVC code so you can preserve logic such as the "Controller" suffix and whatnot.

Read the full discussion online.

To add a post to this discussion, reply to this email (ASPNETWebStack@discussions.codeplex.com)

To start a new discussion for this project, email ASPNETWebStack@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe on CodePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at CodePlex.com


Nov 26, 2012 at 9:55 PM

After doing some research in the code, I would classify this as a design decision that is only valid if assemblies do not change post deploy.  In my case I can change my web.config to pull in controller and area assemblies, hence this breaks me.  I just need to turn this off.

I updated this in the issues area where I have a code fix.

http://aspnetwebstack.codeplex.com/workitem/637

This will allow the original caching to continue to work as is, but lets me turn it off via a web.config setting.

 

Herb

Coordinator
Feb 27, 2013 at 1:52 AM
I had a chat with Levi and Pradeep (both are the devs on the ASP.NET team), and this does appear to be a bug in ASP.NET's compilation system. We have a filed a bug and will try to get this fixed in a future release of ASP.NET.

However, in the meantime, we think we've found a workaround that is worth trying: In the place(s) where you call BuildManager.AddReferencedAssembly and pass in the dynamically-selected assembly, also add a call to BuildManager.AddCompilationDependency and pass in your own string(s) that reflect the identity of the assembly. For example, you could pass in the assembly name or its path or anything like that. Check out MSDN for some more info on this method: http://msdn.microsoft.com/en-us/library/system.web.compilation.buildmanager.addcompilationdependency

It would be great if you could try out this workaround and see if it resolves your problem. Hopefully in the future the bug in ASP.NET will be fixed and this workaround won't be needed anymore.

Can you please let us know if the workaround works?
Mar 2, 2013 at 5:15 PM
Hi all,

Just wanted to chime in here and confirm that this does work for adding assemblies. My issue was similar in that the framework was unable to instantiate controllers that were located in the dynamically accessed assemblies.

I have added the following:
 // Add the plugin as a reference to the application
 BuildManager.AddReferencedAssembly(assembly);
 BuildManager.AddCompilationDependency(assembly.FullName);
And this causes the cache to be rebuilt. There is a caveat: you can't easily remove them. This seems to be the case because:
  1. When you loop through the assemblies and add them to the BuildManager as above after an install, the cache is rebuilt.
  2. When you delete a plugin, there are no new assemblies to add, so the cache is not invalidated.
I am going to attempt one more piece to the workaround, which will be emitting an assembly dynamically with a random guid. I suspect that a bit of code gen/compilation here would be enough to fool the BuildManager.

If anyone has a recommendation otherwise, I'd be happy to try that as well.

What I'd like to see in the "fix" would be something akin to one of the following:
  • A way to easily invalidate the cache like BuildManager.InvalidateAssemblyCache()
  • At the MVC level, a way to dynamically modify the controller cache
If I can find bandwidth I'll fork and start digging into the BuildManager bits, but for now, thanks again for the direction and the above first steps.

Cheers,
-James
Mar 25, 2013 at 10:56 PM
Edited Mar 25, 2013 at 10:58 PM
Hey there,

I know it's been a while but I wanted to follow up here. Below is the code that I use to emit a DLL dynamically and use it to bust the cache. This has to be called in PreApplicationInit along with the other code listed above.

Again, the reason for this is that if you attempt to follow a procedure of 1) install two plugins, 2) remove one plugin, you're not actually adding anything into the mix that is new. Removing the DLLs doesn't send a strong enough message to the controller cache, and there is no method exposed to clear the cache through the build manager.
        private static void BreakCache()
        {
            // start the assembly
            var assemblyName = new AssemblyName { Name = "Cachebuster_" + DateTime.Now.ToString("yyyyMMddhhmmssfff") };
            var filename = assemblyName.Name + ".dll";
            var fullpath = _pluginFolder.FullName + @"\" + filename;

            // create our builders
            var asmBuilder = Thread.GetDomain()
                  .DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, _pluginFolder.FullName);
            var fakeModule = asmBuilder
                  .DefineDynamicModule(assemblyName.Name, filename, true);
            var typeBuilder = fakeModule
                  .DefineType(string.Format("WhoYaGonnaCall{0}", DateTime.Now.ToString("fff")), TypeAttributes.Public);
            var fakeMethod = typeBuilder
                  .DefineMethod("Cachebusters", MethodAttributes.Public | MethodAttributes.Static);

            // emit a minimum amount of IL 
            var methodIL = fakeMethod.GetILGenerator();
            methodIL.Emit(OpCodes.Ret);

            // create the type and save the DLL
            typeBuilder.CreateType();
            asmBuilder.Save(filename);

            var assembly = Assembly.LoadFile(fullpath);

            BuildManager.AddReferencedAssembly(assembly);
            BuildManager.AddCompilationDependency(assembly.FullName);
        }
Two caveats:
  1. This doesn't seem to work with the debugger attached. Visual Studio seems to be hanging onto something (or write protecting, or something) and the PreApplicationInit isn't reliably executed.
  2. Without putting counter-measures in place this is going to grow your directory with two files: a DLL worth about 2 kb and a corresponding PDB weighing in at about 11.5 kb.
A third caveat might be that there is an unperceivable cost at startup to build, save, and finally load the cache-busting DLL.

If anyone has some other way to do this I would love to hear any recommendations.

Thanks again to @eilonlipton and the crew there to pointing out a vector for an interim solution.

Cheers,
-James
Jul 25, 2013 at 10:34 PM
Thanks everyone.
The following workaround works for me.

// Add the plugin as a reference to the application
BuildManager.AddReferencedAssembly(assembly);
BuildManager.AddCompilationDependency(assembly.FullName);

My plugin list is in web.config, so when that changes my PreApplicationInit simply copies and loads what it is told to load. So there isn't really a concept of add or remove here, just restart and do what you are told.

Below is an example of my plugin configs, and I can add and remove the "EveryonePlays" one at will.

<PluginPackages>
<add key="NAK.Reference.Area.App.1.0.0.42" value="" />
<add key="NAK.Reference.Main.App.1.0.0.42" value="Main" />
<add key="NAK.Saml.SSO.1.0.0.42" value="" />
<add key="EveryonePlays.1.0.0.42" value="" />
</PluginPackages>

The above packages each represent a part of an MVC application.
In this case I have the main, and 3 areas. The main is what you get when you make a new MVC app via the wizard, which are the controllers at the root and the shared views.

Thankfully it is all working like I envisioned now.
Thank you Elion and James.