Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Strongly typed Resources in ASP.NET


:P
On this page:

If you’ve used WinForms application you might envy the fact that there you get strongly typed resources automatically for your RESX files – the Compiler automatically generates a class that contains each of the resources as a property of the class. This is cool in many ways – it makes it easier to discover available resources and it minimizes typos in accessing these resources in your code

 

ASP.NET doesn’t have this particular feature at least in stock ASP.NET projects. Part of the reason for this is that Resource management in ASP.NET is a bit more complicated as there are two kinds of resources that exist and also due to the fact that ASP.NET uses resource providers to expose the localization engine rather than a stock ResourceManager. So by default ASP.NET doesn’t have this strongly typed Resources feature.

 

Note though that Web Application Projects – being a more traditional project - incidentally does support strongly typed resources, although you’ll want to be careful with that as these resources are not using the ASP.NET Resource Provider but rather use a ResourceManager so they won’t work with custom providers and cause some possible duplication of resources cached in memory (more on that below).

 

Sooo – I got to thinking that it’s not terribly hard to properly generate strongly typed resources at least for global resources. After all ASP.NET makes it pretty easy to access ASP application resources directly in code via HttpContext.GetGlobalResourceObject() and GetLocalResourceObject() (also exposed on the Control and Page objects).

 

So why only Global Resources? Local Resources are page/control specific and so typically these are use with page specific logic and implicit keys (meta:resourcekey). In theory you could generate resource Ids for some of that content as well (like all non . containing resource keys), but it’s probably not nececessary. Typically local resource files will only contain implicit keys that are automatically assigned by ASP.NET’s implicit resource key (meta:resourcekey) functionality. Note though that you can store plain resources in a local resource file however if you choose. But generating

 

After looking at this initially it turns out that .NET actually includes a class that provides strongly typed resource creation automatically and it’s fairly easy to use and this was my first stop as I figured that would be the way to go. After all why reinvent the wheel, right?  In fact, I thought I could rig this myself by using some tools the StronglyTypedResourceBuilder class to generate the class for me. It’s fairly straight forward to do this and it works with any type of resource manager (I’m using a custom database driven resource set here):

 

/// <summary>

/// Generates a strongly typed assembly from the resources

/// </summary>

/// <param name="ResourceSetName"></param>

/// <param name="Namespace"></param>

/// <param name="Classname"></param>

/// <param name="FileName"></param>

/// <returns></returns>

public bool CreateStronglyTypedResource(string ResourceSetName,string Namespace,

                                        string Classname, string FileName)

{

    try

    {

        //wwDbResourceDataManager Data = new wwDbResourceDataManager();

        //IDictionary ResourceSet = Data.GetResourceSet("", ResourceSetName);

 

        // *** Use the custom ResourceManage to retrieve a ResourceSet

        wwDbResourceManager Man = new wwDbResourceManager(ResourceSetName);

        ResourceSet rs = Man.GetResourceSet(CultureInfo.InvariantCulture, false, false);

        IDictionaryEnumerator Enumerator = rs.GetEnumerator();

 

        // *** We have to turn into a concret Dictionary

        Dictionary<string, object> Resources = new Dictionary<string, object>();

        while (Enumerator.MoveNext())

        {

            DictionaryEntry Item = (DictionaryEntry) Enumerator.Current;

            Resources.Add(Item.Key as string, Item.Value);

        }

       

        string[] UnmatchedElements;

        CodeDomProvider CodeProvider = null;

 

        string FileExtension = Path.GetExtension(FileName).TrimStart('.').ToLower();

        if (FileExtension == "cs")

            CodeProvider = new Microsoft.CSharp.CSharpCodeProvider();

        else if(FileExtension == "vb")

            CodeProvider = new Microsoft.VisualBasic.VBCodeProvider();

 

        CodeCompileUnit Code = StronglyTypedResourceBuilder.Create(Resources,

                                           ResourceSetName, Namespace, CodeProvider, false, out UnmatchedElements);

 

        StreamWriter writer = new StreamWriter(FileName);

        CodeProvider.GenerateCodeFromCompileUnit(Code, writer, new CodeGeneratorOptions());

        writer.Close();

    }

    catch (Exception ex)

    {

        this.ErrorMessage = ex.Message;

        return false;

    }

 

    return true;

}

 

This can be called with:

 

Exporter.CreateStronglyTypedResource("resources",

"AppResources","Resources",

Server.MapPath("App_Code/Resources/Resources.cs"));

 

This worked like a charm, but alas it creates a class that calls the ResourceManager and not the ASP.NET ResourceProvider. It looks something like this:

 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]

    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]

    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]

    public class Resources {

       

        private static global::System.Resources.ResourceManager resourceMan;

       

        private static global::System.Globalization.CultureInfo resourceCulture;

       

        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]

        internal Resources() {

        }

       

        /// <summary>

        ///   Returns the cached ResourceManager instance used by this class.

        /// </summary>

        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]

        public static global::System.Resources.ResourceManager ResourceManager {

            get {

                if (object.ReferenceEquals(resourceMan, null)) {

                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AppResources.Resources", typeof(Resources).Assembly);

                    resourceMan = temp;

                }

                return resourceMan;

            }

        }

       

        /// <summary>

        ///   Overrides the current thread's CurrentUICulture property for all

        ///   resource lookups using this strongly typed resource class.

        /// </summary>

        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]

        public static global::System.Globalization.CultureInfo Culture {

            get {

                return resourceCulture;

            }

            set {

                resourceCulture = value;

            }

        }

       

        /// <summary>

        ///   Looks up a localized string similar to Could not create new customer.

        /// </summary>

        public static string CouldNotCreateNewCustomer {

            get {

                return ResourceManager.GetString("CouldNotCreateNewCustomer", resourceCulture);

            }

        }

       

        /// <summary>

        ///   Looks up a localized string similar to Could not load customer.

        /// </summary>

        public static string CouldNotLoadCustomer {

            get {

                return ResourceManager.GetString("CouldNotLoadCustomer", resourceCulture);

            }

        }

       

        /// <summary>

        ///   Looks up a localized string similar to Customer was saved.

        /// </summary>

        public static string CustomerSaved {

            get {

                return ResourceManager.GetString("CustomerSaved", resourceCulture);

            }

        }

       

        public static System.Drawing.Bitmap Sailbig {

            get {

                object obj = ResourceManager.GetObject("Sailbig", resourceCulture);

                return ((System.Drawing.Bitmap)(obj));

            }

        }

       

        /// <summary>

        ///   Looks up a localized string similar to Today.

        /// </summary>

        public static string Today {

            get {

                return ResourceManager.GetString("Today", resourceCulture);

            }

        }

       

        /// <summary>

        ///   Looks up a localized string similar to Yesterday.

        /// </summary>

        public static string Yesterday {

            get {

                return ResourceManager.GetString("Yesterday", resourceCulture);

            }

        }

    }

}

 

It worked great generating this class, but unfortunately this class won’t work with ASP.NET because it hardcodes the REsourceManager assignment and the code makes an assumption that the assembly that hosts the generated class also hosts the resources – an assumption that unfortunately is not true in ASP.NET. In ASP.NET Global Resources are stored in APP_GLOBALRESOURCESXXXX.dll. So this approach simply doesn’t work.

 

Web Application Projects – Strongly typed Global Resources for free - Almost

As I mentioned earlier WAP automatically creates strongly typed resources for global resources, creating one class for each resource file. In WAP this process actually works because ASP.NET creates this assembly by combining the resources and the generated resource class wrapper into the APP_GLOBALRESOURCESXXXX.DLL assembly. By placing the file there the stock generator actually works and the following initialization code works:

 

ResourceManager rm = ResourceManager("AppResources.Resources", typeof(Resources).Assembly);

 

However -  note that this ResourceManager will be a duplicate of the ResourceManager that the ASP.NET ResourceProvider may already be using to access ResX resources. So if you access any resources through HttpContext.GetGlobalResource() you are now holding two ResourceSets in memory – one for the ResourceProvider and one for the ResourceManager. In addition these resources also will not work if you use a separate ResourceProvider since it bypasses ASP.NET’s resource provider altogether and goes straight to the ResourceManager.

 

So be wary of using these strongly typed resources or at the very least make sure that you use only the strongly typed resources or only GetGlobalResourceObject to minimize Resource cache duplication.

Creating Resources Properly

So the above is interesting, but it’s obviously not the right way to return provide strongly typed Resource data. The way that this should work is that each of these properties returns the result from HttpContext.GetGlobalResourceObject() so that it always goes through the ASP.NET provider which would ensure that Resource access always works whether you’re going through the default ResX provider or a custom provider and that there is no resource cache duplication. This mechanism should work both with stock projects and WAP.

 

So as part of my resource provider and general ASP.NET localization utilitities I added some functionality to create strongly typed resources from Global Resources that look something like this:

 

using System;

using System.Web;

 

namespace AppResources

{

      public class Myresource

      {

            public static System.String Home

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Myresource","Home"); }

            }

 

            public static System.String HelloWorld

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Myresource","HelloWorld"); }

            }

 

      }

 

      public class Resources

      {

            public static System.String Yesterday

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Resources","Yesterday"); }

            }

 

            public static System.Drawing.Bitmap Sailbig

            {

                  get { return (System.Drawing.Bitmap) HttpContext.GetGlobalResourceObject("Resources","Sailbig"); }

            }

 

            public static System.String Today

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Resources","Today"); }

            }

 

            public static System.String CustomerSaved

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Resources","CustomerSaved"); }

            }

 

            public static System.String CouldNotCreateNewCustomer

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Resources","CouldNotCreateNewCustomer"); }

            }

 

            public static System.String CouldNotLoadCustomer

            {

                  get { return (System.String) HttpContext.GetGlobalResourceObject("Resources","CouldNotLoadCustomer"); }

            }

 

      }

 

}

 

 

The tool will look at all Global resources in APP_GlobalResources .resx files (or my Database provider or a raw ResourceSets) and then convert them into classes, one class for each resource set. Each class has properties for each resourcekey and it simply returns a strongly typed value for the GetGlobalResourceObject() call.

 

There are routines that can create one class per ResourceSet or create all ResourceSets as shown above (2 resourcesets) and you can use either ResX files as input or the Database Provider that is related to this library.

 

There are one little kink in generating all ResourceSets at once  –the class name is generated based on the resource name, but it’s made ‘proper’ case to ensure that the resource name stays the same regardless what case was used for the actual file or resourcesetname in the database. This is primarily a workaround for a decision I made in the database provider to use all lowercase storage in the database and for resx exports. Proper case is slightly more readable but for longer resource set names this will still not be optimal.

 

To generate the resource class you can run a couple of lines of code:

 

StronglyTypedWebResources Exp = new StronglyTypedWebResources(Request.PhysicalApplicationPath);

 

string Class =Exp.CreateClassFromAllResXResources("AppResources", Server.MapPath("~/App_Code/Resources.cs")));

 

 

or you can create an individual class from a single resource:

 

Clas = Exp.CreateClassFromResXResource(

            Server.MapPath("~/App_GlobalResources/resources.resx"),

            "AppResources",

            "Resources",

            Server.MapPath("~/App_Code/Resources.cs") );

 

Specifying a .cs or .vb file will create the appropriate class. Other methods allow doing the same export from the DataBase resources or by running against a live ResourceSet object which means you can pretty much convert any resource file as long as there’s a ResourceManager or Provider that can provide the ResourceSet.

 

Parsing ResX files

Parsing ResX files is pretty trivial to do so for the ResX version the code looks something like this:

 

/// <summary>

/// Creates a class containing strongly typed resources of all resource keys

/// in all global resource ResX files. A single class file with multiple classes

/// is created.

///

/// The extension of the output file determines whether VB or CS is generated      

/// </summary>       

/// <param name="Namespace"></param>

/// <param name="FileName">Output file name for the generated class. .cs and .vb generate appropriate languages</param>

/// <returns>Generated class as a string</returns>

public string CreateClassFromFromAllGlobalResXResources(string Namespace, string FileName)

{

    if (!this.WebPhysicalPath.EndsWith("\\"))

        this.WebPhysicalPath += "\\";

   

    bool IsVb = this.IsFileVb(FileName);

 

    string ResPath = WebPhysicalPath + "app_globalresources\\";

 

    string[] Files = Directory.GetFiles(ResPath, "*.resx");

 

    StringBuilder sbClasses = new StringBuilder();

 

    foreach (string CurFile in Files)

    {

        string file = CurFile.ToLower();

        string[] tokens = Path.GetFileName(file).Split('.');

 

        // *** If there's more than two parts is a culture specific file

        // *** We're only interested in the invariant culture

        if (tokens.Length > 2)

            continue;

 

        // *** ResName: admin/default.aspx or default.aspx or resources (global or assembly resources)

        string LocaleId = "";

        string ResName = Path.GetFileNameWithoutExtension(tokens[0]);

        ResName = ResName.Replace(".", "_");

        ResName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(ResName);

 

        string Class = this.CreateClassFromResXResource(file, Namespace, ResName, null);

        sbClasses.Append(Class);

    }

 

    string Output = this.CreateNameSpaceWrapper(Namespace, IsVb, sbClasses.ToString());

    File.WriteAllText(FileName,Output);

 

    return Output;

}

 

 

/// <summary>

/// Creates an ASP.NET compatible strongly typed resource from a ResX file in ASP.NET.

/// The class generated works only for Global Resources by calling GetGlobalResourceObject.

///

/// This routine parses the raw ResX files since you can't easily get access to the active

/// ResourceManager in an ASP.NET application since the assembly is dynamically named and not

/// easily accessible.

/// </summary>

/// <param name="ResourceSetFileName"></param>

/// <param name="Namespace"></param>

/// <param name="FileName">Output filename for the CSharp class. If null no file is generated and only the class is returned</param>

/// <returns></returns>

public string CreateClassFromResXResource(string ResXFile, string Namespace, string Classname, string FileName)

{

    XmlDocument Dom = new XmlDocument();

 

    bool IsVb = this.IsFileVb(FileName);

   

    try

    {

        Dom.Load(ResXFile);

    }

    catch (Exception ex)

    {

        this.ErrorMessage = ex.Message;

        return null;

    }

 

    string Indent = "\t\t";

    StringBuilder sbClass = new StringBuilder();

 

    CreateClassHeader(Classname, IsVb, sbClass);

    XmlNodeList nodes = Dom.DocumentElement.SelectNodes("data");

 

    foreach (XmlNode Node in nodes)

    {

        string Value = Node.ChildNodes[0].InnerText;

        string ResourceId = Node.Attributes["name"].Value;

 

        string TypeName = null;

        if (Node.Attributes["type"] != null)

            TypeName = Node.Attributes["type"].Value;

 

        if (!string.IsNullOrEmpty(TypeName))

        {

            // *** File based resources are formatted: filename;full type name

            string[] tokens = Value.Split(';');

            if (tokens.Length > 0)

                // *** Grab the type and get the full name

                TypeName = Type.GetType(tokens[1]).FullName;

        }

        else

            TypeName = "System.String";

 

        // *** It's a string

        if (!IsVb)

        {

            sbClass.Append(Indent + "public static " + TypeName + " " + ResourceId + "\r\n" + Indent + "{\r\n");

            sbClass.AppendFormat(Indent + "\tget {{ return ({2}) HttpContext.GetGlobalResourceObject(\"{0}\",\"{1}\"); }}\r\n",

                                 Classname, ResourceId,TypeName);

            sbClass.Append(Indent + "}\r\n");

            sbClass.Append("\t}\r\n\r\n");

        }

        else

        {

            sbClass.Append(Indent + "Public Shared Property " + ResourceId + "() as " + TypeName + "\r\n");

            sbClass.AppendFormat(Indent + "\tGet\r\n" + Indent + "\t\treturn CType( HttpContext.GetGlobalResourceObject(\"{0}\",\"{1}\"), {2})\r\n",

                                 Classname, ResourceId,TypeName);

            sbClass.Append(Indent + "\tEnd Get\r\n");

            sbClass.Append(Indent + "End Property\r\n\r\n");

        }

    }

 

    if (!IsVb)

        sbClass.Append("\t}\r\n\r\n");

    else

        sbClass.Append("End Class\r\n\r\n");

   

 

    if (!string.IsNullOrEmpty(FileName))

    {

        string FileContent = this.CreateNameSpaceWrapper(Namespace, IsVb, sbClass.ToString());

        File.WriteAllText(FileName, FileContent);

        return FileContent;

    }

 

    return sbClass.ToString();

 

}

 

 

There are a couple of helper dependencies (provided in the library download) but you can get the jist of what’s happening here. The code basically runs through each of the data nodes of the XML document, picks up each key and type and based on that creates the property Get methods that return HttpContext.GetGlobalResourceObject().

 

 

For the ResX provider it took me a while of searching to figure out that ASP.NET has no way from a running application to get an instance of the ResourceProvider or ResourceManager. I searched high and low and although those properties are available the ResourceExpressionBuilder, they are marked internal and inaccessible (well, I guess Reflection could have gotten me access <s>) to an application. I suppose this makes some sort of sense since ASP.NET’s provider model doesn’t require a ResourceManager – you can implement a new provider and only implement the provider interface without ever creating the more complex and messy ResourceManager ‘implementation’ (yeah – no interface there!). But it sure would be helpful to be able to at least get a reference to the ResourceProvider and ResourceSet it contains to allow retrieving all the resource keys at runtime. I couldn’t find a way to do this except by hooking the provider. So instead I opted for the more brute force mechanism of parsing the ResX file directly. It’s easy enough to do if not as elegant.

 

Anyway, I’ve added this code to my ResourceProvider application sample and there’s a sample page in the LocalziationAdmin folder (StronglyTypedGlobalResources.aspx) that can generate these resources as shown above as well as the provider and utility source code.

 

You can download the latest version of the provider including this code from:

 

http://www.west-wind.com/files/confererences/conn_Localization.zip

 

There are a few other enhancements in this update:

 

Support for Importing ResX resources into the Resource Database

ResourceId renaming and Implicit Key renaming (renaming a group of keys)

Updated layout

A few bug fixes

 

Documentation can be found here:

http://www.west-wind.com/tools/wwDbResourceProvider/docs/index.htm

 

 

Enjoy

Posted in ASP.NET  Localization  

The Voices of Reason


 

Rick Strahl's Web Log
December 07, 2006

# ASP.NET Connections Fall 2006 Slides and Samples posted - Rick Strahl's Web Log

I’ve posted my slides and samples for my Fall 2006 ASP.NET Connections sessions online.

jiangyh
December 15, 2006

# re: Strongly typed Resources in ASP.NET

hi Rick Strahl's

My name is jiangyh, a Chinese developer.
I want translate this article to chinese and shared it for others Chinese developer.

May I do so?

thanks a lot

Visual Studio.NET
April 24, 2007

# codezone-si.info - Resource file


Mike
June 25, 2007

# re: Strongly typed Resources in ASP.NET

Hi Rick,
the link to the latest version seems to be broken. Right?

Mike

Carl Nelson
June 30, 2007

# re: Strongly typed Resources in ASP.NET

Hi, Mike.

The address shown is http://www.west-wind.com/files/confererences/conn_Localization.zip. The actual location is http://www.west-wind.com/files/conferences/conn_Localization.zip (the word "conferences" is misspelled).

HTH,
Carl


July 08, 2007

# 本周ASP.NET英文技术文章推荐[11/26 - 12/02] - Dflying Chen @ cnblogs - 博客园


Craig Serold
July 10, 2007

# re: Strongly typed Resources in ASP.NET

Hi Rick,

I was thinking I could create a class library project, add my resource files to this project, generate the strong names assembly resource files and then reference this class library project from all my other projects that need access to the resources. I noticed that VS actually creates the resource classes as "internal" classes, preventing me from using the assembly in external projects. Is there a reason for this? You have any thoughts on it?

Thanks so much,
Craig

Adriana
January 11, 2010

# re: Strongly typed Resources in ASP.NET

I really wish I could picture this :"going through the default ResX provider or a custom provider ", a custom povider?? hmm

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024