Custom Localization ResourceProviders and Compiler Errors
I’ve been struggling with building a custom ResourceManager and hooking it into ASP.NET over the last few days as part of a localization engine that is data driven rather than through Resources. I’ve had a separate engine for this for sometime, but it hasn’t been hooked into the .NET or ASP.NET infrastructure so went back and tried to figure out how to build a custom resource manager and resource provider to hook it into ASP.NET.
This has been a pretty painful experience for me. The ResourceManager class is not well setup for extensibility – no interfaces, you have to basically inherit and selectively override methods in various classes (ResourceManager, ResourceSet, ResourceReader, ResourceWriter). With the help of some code I found I was able to make that work. The ASP.NET interfaces ResourceProvider and ResourceProviderFactory are a little better, but not much – also no interfaces but at least these interfaces are relatively simple and it’s easy to see what needs to be overridden. So after a painful few days I managed to get my code hooked up and running and serving resources.
It works in the development project and I can run the page just fine if I use in place compilation that VS.NET uses in development. However, when I compile the project I get an error like this:
:
error ASPPARSE: /LocalizationTest.aspx(Line 27)
LinkButton1 looks something like this:
<asp:LinkButton ID="LinkButton1" runat="server" Text="<%$ Resources:LinkButton1Resource1.Text %>" meta:resourcekey="LinkButton1Resource2"></asp:LinkButton>
Which is an explicit expression and as I mentioned this works fine in dev mode. At pre-compile time however this fails outright.
ASP.NET compiles pages and does something like this for the parse tree:
private LinkButton __BuildControlLinkButton1()
{
LinkButton button1 = new LinkButton();
base.LinkButton1 = button1;
button1.ApplyStyleSheetSkin(this);
button1.ID = "LinkButton1";
button1.Text = Convert.ToString(base.GetLocalResourceObject("LinkButton1Resource1.Text"), CultureInfo.CurrentCulture);
return button1;
}
Notice that it assigns the resource directly by calling the base.GetLocalResourceObject, which in turn calls the local resource manager. That’s reasonable, but it doesn’t explain WHY the compiler would need to have access to this resource.
So I took out my custom provider and compiled the Resource based version. Now it too will fail if there’s a missing explicit or implicit resource in the base (Invariant) resource file. Apparently ASP.NET tries to be overly helpful by checking and making sure the resource exists. This must be some sort of explicit check though because nothing that gets compiled into the page really changes in this respect. This is a compiler issue.
So now that I sort of understand how this works I can see that the ResourceManager needs to be accessible and working at Compile time. How freaking LAME is that? That pretty much defeats any dynamic mechanism for loading resources at runtime.
Ok, fine, to see if I could get this to work I hardcoded my connection string into the ResourceManager (not realistic I know for a real application but I’ll worry about that later) and then hooked it back up and re-ran the compile. I also hooked up SQL Profiler to see if it’s hitting the database and what it’s asking for. Sure enough the database gets hit and the compiler is asking for the en-us and en ResourceSets which should work fine. Both of these include the resource that the precompiler is failing on. In theory at least my provider should be feeding the appropriate resource set now and the compiler should be finding the correct resource string.
But it’s still failing…
This is looking more and more like I wasted 2 days working on this because of a bad compiler check implementation. The code runs just fine, but the damn compiler isn’t happy because it find resources at compile time.
I guess one solution to this debacle is to use Web Application Projects which doesn’t pre-compile the ASPX pages and defers this process until runtime where it appears to work correctly. But then using Web Deployment Projects on the project won’t work…
Has anybody been down this path and figured out a way around this mess?
Other Posts you might also like
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Resolving Paths To Server Relative Paths in .NET Code
- Map Physical Paths with an HttpContext.MapPath() Extension Method in ASP.NET
- Getting the Client IP Address in ASP.NET Core
- Reading Raw ASP.NET Request.Body Multiple Times
The Voices of Reason
# re: Custom Localization ResourceProviders and Compiler Errors
ASP.NET checks that a *neutral* resource exists. It doesn't care whether you have a localized resource for a given property. It does this just to make sure you have no typos. I can see how it could be frustrating, but we felt this was a lesser evil.
Also if you'd like to see a sample localization provider I wrote, there's on here: http://www.leftslipper.com/ShowFaq.aspx?FaqId=9
Thanks,
Eilon
PS: I'm having trouble with your CAPTCHA. It took me about 4 refreshes to get it to display. It kept rendering as an invalid image.
# re: Custom Localization ResourceProviders and Compiler Errors
The reason for this is basically an optimization: controls have many properties, and it would be very expensive to attempt calling GetLocalResourceObject() for each of them just in case it exists at runtime. So we took the approach that to use localization for a specific attribute, you need to 'declare' your intent by returning something at compile time.
I grant you that this makes it somewhat less dynamic, but that was the price to pay to get acceptable perf.
# re: Custom Localization ResourceProviders and Compiler Errors
http://www.codeproject.com/aspnet/customsqlserverprovider.asp?msg=1674170
and I see one of the things happening there is to also autofill a value if none is found.
I'm not sure how safe that is to automatically try and 'make up' a blank value though (don't have it running just yet <s>) because I think that might cause some problems with Resource fallback.
Also, I'm not clear on what order these calls are fired - it looks (from what I've seen so far) that the invariant resource is called first and the the culture specific ones after that. Hmmm... it might be the compilation step that's actually doing the Invariant calls. I'll have to take a closer look once I get this new simpler provider running - it'll be easier to see what's going on with that.
David, I think if a specific resource doesn't exist the ASP.NET compiler fails altogether and won't compile the page. (I get a parser error: The resource object with key 'LinkButton1Resource1.Text' was not found in the page in an inplace page).
# re: Custom Localization ResourceProviders and Compiler Errors
<asp:Label runat="server" meta:resourcekey="Label1Resource1"/>
Here, you will get different code generated based on what the resource provide returns at compile time. e.g. it will generate a GetLocalResourceObject() assignment for teh Text property if and on if the provider returns non null for the key "Label1Resource1.Text".
# re: Custom Localization ResourceProviders and Compiler Errors
D:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\ildasm.exe
There you can see all the resources and the most important , the namespace for resources. As far as i remember all issues i had with resources was in wrong namespace, because when you getting any resource, you getting it with namespace+name. Check it please.
# re: Custom Localization ResourceProviders and Compiler Errors
I was wondering how to make use Server Resource to feed the resource texts to JS scripts or is there any other hack ??
I guess, anybody out here can understand this problem ?
# re: Custom Localization ResourceProviders and Compiler Errors
# re: Custom Localization ResourceProviders and Compiler Errors
using System; using System.Web.Compilation; using System.Globalization; using System.Resources; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Xml; using System.Web; namespace CreativePlug.Providers { public sealed class XmlResourceProviderFactory : ResourceProviderFactory { public XmlResourceProviderFactory() { } public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return new XmlResourceProvider(null, classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { virtualPath = this.GetVirtualPath(virtualPath); return new XmlResourceProvider(virtualPath, null); } public class XmlResourceProvider : IResourceProvider { private string _virtualPath; private string _className; private IDictionary _resource; public XmlResourceProvider(string virtualPath, string className) { _virtualPath = virtualPath; _className = className; } private IDictionary GetResource(string cultureName) { _resource = new ListDictionary(); IDictionary resourceDict = _resource as IDictionary; resourceDict = XmlResourceHelper.GetResources(_virtualPath, _className, cultureName, false, null); _resource = resourceDict; return resourceDict; } public object GetObject(string resourceKey, CultureInfo culture) { if (culture == null || culture == CultureInfo.InvariantCulture) culture = CultureInfo.CurrentUICulture; object value = GetResource(culture.Name)[resourceKey]; return value; } public System.Resources.IResourceReader ResourceReader { get { return new XmlResourceReader(GetResource(CultureInfo.CurrentUICulture.Name)); } } } private sealed class XmlResourceReader : IResourceReader { private IDictionary _resources; public XmlResourceReader(IDictionary resources) { _resources = resources; } IDictionaryEnumerator IResourceReader.GetEnumerator() { return _resources.GetEnumerator(); } void IResourceReader.Close() { } IEnumerator IEnumerable.GetEnumerator() { return _resources.GetEnumerator(); } void IDisposable.Dispose() { } } internal static class XmlResourceHelper { public static IDictionary GetResources(string virtualPath, string className, string cultureName, bool designMode, IServiceProvider serviceProvider) { ListDictionary resources = new ListDictionary(); if (!String.IsNullOrEmpty(virtualPath)) { // Get Local resources XmlDocument configurationFile = new XmlDocument(); string xmlFilePath = HttpContext.Current.Server.MapPath(HttpContext.Current.Request.ApplicationPath + "/LocalResources/" + virtualPath + "_" + cultureName + ".xml"); configurationFile.Load(xmlFilePath); XmlNodeList parentNodeList = configurationFile.SelectNodes("Resource/item"); foreach (XmlNode childNode in parentNodeList) { string rn = childNode.Attributes.Item(0).Value.ToString(); string rv = childNode.InnerText; resources.Add(rn, rv); } } else if (!String.IsNullOrEmpty(className)) { // Get Global resources XmlDocument configurationFile = new XmlDocument(); string xmlFilePath = HttpContext.Current.Server.MapPath(HttpContext.Current.Request.ApplicationPath + "/GlobalResources/Global." + cultureName + ".xml"); configurationFile.Load(xmlFilePath); XmlNodeList parentNodeList = configurationFile.SelectNodes("Resource/item"); foreach (XmlNode childNode in parentNodeList) { string rn = childNode.Attributes.Item(0).Value.ToString(); string rv = childNode.InnerText; resources.Add(rn, rv); } } return resources; } } private string GetVirtualPath(string virtualPath) { string virtualFileName = System.IO.Path.GetFileName(virtualPath); string virtualFileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(virtualPath); virtualPath = virtualPath.Remove(0, 1); virtualPath = virtualPath.Replace(HttpContext.Current.Request.ApplicationPath, ""); virtualPath = virtualPath.Replace("/", "_"); virtualPath = virtualPath.Replace(virtualFileName, virtualFileNameWithoutExtension); virtualPath = virtualPath.Replace("Localization_", ""); return virtualPath; } } }
# re: Custom Localization ResourceProviders and Compiler Errors
# re: Custom Localization ResourceProviders and Compiler Errors
# re: Custom Localization ResourceProviders and Compiler Errors
I think if I remember right you can get at the path using the VirtualPathProvider even during compilation. Take a look at the ImplicitResourceProvider for the design time implementation - it does something similar to figure the base paths for the application.
# re: Custom Localization ResourceProviders and Compiler Errors
object IResourceProvider.GetObject(string resourceKey, CultureInfo culture) { if (culture == null || culture == CultureInfo.InvariantCulture) culture = CultureInfo.CurrentUICulture; object value = GetResource(culture.Name)[resourceKey]; if (value == null) return ""; return value; }
FYI, the following worked for me...
Replaced...
HttpContext.Current.Server.MapPath
with...
System.Web.Hosting.HostingEnvironment.MapPath
Replaced...
HttpContext.Current.Request.ApplicationPath
with....
System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath
# re: Custom Localization ResourceProviders and Compiler Errors
Here is the link
http://www.hanselman.com/blog/AmIRunningInDesignMode.aspx
# re: Custom Localization ResourceProviders and Compiler Errors
Let me tell you what I did so far. I am using custom resource provider and my resource files(plain .resx file not a satellite assembly) are residing somewhere in file system(so that other application share this .resx file). If I ran the web application, It works fine without any run time error but when I try to build the application it gives me an compliation error "resource object with key 'key' was not found'. You have ran into the same error so probably you can easily give me some idea what could be the wrong.
I have spent couple days but not able to fix this error.
Thanks
S.
# re: Custom Localization ResourceProviders and Compiler Errors
{
if (AspHelpers.ASPNetCompilerExecutionContext)
return "ASPNetCompilerDesignTimeExecutionContext";
----------
where:
public static bool ASPNetCompilerExecutionContext
{
get
{
string entryMethod = (new StackTrace()).GetFrame((new StackTrace()).FrameCount - 1).GetMethod().Name;
if (entryMethod == "PrecompileApp")
return true;
else
return false;
}
}
# re: Custom Localization ResourceProviders and Compiler Errors
I am trying sample code available at http://www.west-wind.com/WestwindWebToolkit/Westwind.GlobalizationWeb/default.aspx
When i implement same in my web site, it gives "object reference not set to an instance of an object" at the time of compilation.
If i host sample code in IIS, it also start giving same error.
Can anyone please suggest solution for it?
Thanks,
# re: Custom Localization ResourceProviders and Compiler Errors
http://west-wind.com/Westwind.Globalization/
Much improved and less configuration you have to deal with - same tool, just a newer version and easier integration.
# re: Custom Localization ResourceProviders and Compiler Errors
I can override The IResourceProvider.GetObject method and catch any NULL returns and make it return an empty value instead. Now the app compiles properly:
object IResourceProvider.GetObject
(string resourceKey, CultureInfo culture)
{
if (culture == null || culture == CultureInfo.InvariantCulture)
culture = CultureInfo.CurrentUICulture;
object value = this.ResourceManager.GetObject(resourceKey,culture);
if (value == null)
return "";
return value;
}
Now this works, but it's a problem for any non-string resources unfortunately, so I'd need to check for specific type information here.