Tuesday, June 16, 2009

Updatable ASP.NET ResX Resource Provider – yes, it’s possible!

Update (November 4, 2009): I’ve been getting a lot of emails over this one; if you’re running into any issues implementing this, however slight, please post here and I will answer the questions for everyone. Also, note that, with a few minor changes, you can use CompilationMode to introduce content/layout changes directly on *.aspx/*.ascx/*.master surfaces at run-time as well.

First and foremost, I want to thank Rick Strahl for his efforts; his work served as the initial inspiration for us to even consider this approach [1]. Next, I want to highlight that our motivations for deviating from the standard (resource) providers differ somewhat: in our case, we had a very high-volume application that was already built-out with the default *.resx localization scheme, and we were more or less content with it, except for one drawback. Our motivation was solely driven by the need to push updates to production without: a) restarting the application and jeopardizing in-process session, cache data; and b) introducing any sort of performance degradation, however slight.

Editing Resources at Runtime

Think of this as a make-shift CMS, if you will, that enables you to update localized content in your ASP.NET applications without full-blown code promotions (and the attendant scheduling considerations).

Beyond the ability to update values in production, I’m not convinced that a custom authoring UI justifies some of the risks introduced in the DB-based solutions; certainly, some of the value-added features are nice-to-haves, but the focus is on remaining feature-compatible with the built-in provider (e.g. support for Visual Studio’s 'Generate Local Resources', Explicit and Implicit Resource Expressions, etc.) while providing run-time updatability.

That’s really all that we’re after and the simplicity (foot-print) of the solution should reflect it.

In this vein, we set out to deliver a scheme that allows you to preserve your *.resx files with the added ability to push updates to production app pools; in doing so, we needed an understanding of:

  1. Why web resources compiled from *.resx to *.resources in the first place?
    • Why would a web applications store binary in these files – images, audio, text-files, etc?
  2. How do we by-pass this to apply localization directly from cached *.resx files (with the necessary disk-based cache dependencies)?
  3. How do we stop the automatic assembly generation that eventually kills the app pool?
  4. How do we stop the FCNs for .NET special directories but continue to maintain FCNs for CacheDependencies?

We begin by first registering our custom provider in web.config:

<globalization resourceProviderFactoryType="Sample.UpdatableResXResourceProviderFactory, Sample" />

This, along with the reference assembly, is all that’s required to introduce this to an existing application.

[Sections below assume familiarity with File Change Notifications (FCNs) in ASP.NET.]

Next, we spent some time looking at how one would disable FCNs to allow *.resx files to be pushed to their default paths (for both local and global resources) without triggering assembly generation. A few queries proved that this is indeed possible with some crude reflection; the following hack, for instance, allows you to kill all FCNs across the application (once placed in Application_Start):

Untitled

PropertyInfo p = typeof(System.Web.HttpRuntime).GetProperty("FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); object o = p.GetValue(null, null); FieldInfo f = o.GetType().GetField("_FCNMode", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase); f.SetValue(o, 1);

Unfortunately, this also has the nasty side-effect of killing the very FCNs that your Cache Dependencies rely on to reload file contents. There is a slight variation that’s successful in preserving the Cache Dependency FCNs but disabling virtually everything else:

PropertyInfo p = typeof(System.Web.HttpRuntime).GetProperty("FileChangesMonitor", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); object o = p.GetValue(null, null); FieldInfo f = o.GetType().GetField("_dirMonSpecialDirs", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase); f.SetValue(o, null);

More on the need to unload the AppDomain [2], [3].

We were ultimately unhappy with this solution for several reasons and sought more simplicity. (We also had issues updating global resource files in their native paths on precompiled builds.) Recognizing that App_Data is one of a few folders under the web root that’s shielded from FCNs, we decided to move our *.resx output (preserving their relative paths) to App_Data\Resources. Under this new location, we don’t need reflection to interfere with the FileChangeMonitor and can continue to push intact *.resx files to the web root without generating FCNs or triggering assembly builds.

Ultimately, it’s really no different than pushing out any *.xml updates to App_Data except in this case the intact *.resx files that you’ve already developed serve as the payload.

Again, a key principle here was to minimize foot-print. The gist of it really boils down to a few lines of code, with the necessary dressing to plug it in. The abstract UpdatableResXResourceProvider contains all of the logic for both providers - the GlobalResXResourceProvider and the LocalResXResourceProvider simply provide path details for their respective locations:

image

The implementation boils down to the GetResourceCache method that leverages the framework’s own ResXResourceReader to read the original *.resx, storing the result it in the runtime cache with a new CacheDependency:

image

Download: UpdatableResXResourceProviderFactory.zip

Special Instructions:

  • Update the DefaultStore = @"App_Data/Resources/" parameter as a first-step; to use the default location, you need a post-build process that propagates *.resx files to this location (preserving their relative depth). To test with resources in their current location, you can use DefaultStore = “”;
  • Note that a reference to System.Windows.Forms is required for referencing ResXResourceReader.

References:

[1] – Updated Westwind.Globalization Data Driven Resource Provider for ASP.NET
http://www.west-wind.com/weblog/posts/695968.aspx

[2] – Exactly which files and directions are monitored http://blogs.msdn.com/tmarq/archive/2007/11/02/asp-net-file-change-notifications-exactly-which-files-and-directories-are-monitored.aspx

[3] – On the need to throttle updates, even if down by a scheduled task after hours: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=366809

“This is a limitation of handling file change notifications from the operating system. If too many files change, the internal change buffers overflow and ASP.NET has to assume that the application needs to be restarted. Unfortunately, there is no workaround other than to throttle the rate at which changed files are propagated to a live web application.”

[4] – Stop Monitoring on Designated Folders
http://www.velocityreviews.com/forums/t533767-workaround-for-fcndirectory-delete-bug.html

[5] – Error Debugging Project (KB 810886)
http://forums.asp.net/p/939042/1118849.aspx

[6] – ASP.NET 2.0: Custom Resource Provider Using Sql Database (Support for Oracle)
http://asp-net-whidbey.blogspot.com/2006/03/aspnet-20-custom-resource-provider.html

5 comments - Subscribe!

Scott Hanselman said...

Nicely done!

Nariman Haghighi said...

@Scott: Thanks!

@Franklin: Thanks for pointing that out - I've uploaded a fix for the ContainsIgnoreCase. RE: GetResourceCache(), I don't see an indexed property exposed; if you find that you can enumerate it more efficiently, please post your solution here.

Prashant Atal said...

Can you please provide me some more info on how can i implement in one of my ASP.NET apps with some simple steps. I read the complete post but did not under how can it be simply implemented.

Regards
Prashant Atal

Nariman Haghighi said...

@Franklin: Yes, of course. You're right. I haven't looked at this code in a while (knee deep in something else at the moment). Resources can be enumerated by traversing the IDictionaryEnumerator returned by the GetEnumerator method, which is all that the default implementation does. But if you look at the implementation of BasicResXResourceReader in the sample above, it's trivial to put together an IResourceReader that exposes IDictionary capabilities for constant lookup. You would have to cast the result of GetResourceCache as the specialized type to gain access to the IDictionary. I will try to update the code sample this week.

franklin said...

@Nariman

In your code, GetResourceCache() returns an IResourceReader.

ResXResourceReader cannot be cast to IDictionary.
How can this be converted?

The BasicResXResourceReader implementation takes an IDictionary and exposes it as an IDictionaryEnumerator, not the other way around.