Extending the ASP.NET 2.0 Resource-Provider Model

 

Michèle Leroux Bustamante

IDesign Inc

October 2006

Applies to:
   Microsoft ASP.NET 2.0
   Microsoft Visual Studio 2005
   Localization

Summary: Microsoft ASP.NET 2.0 unleashed a number of wonderful improvements for localizing Web applications. Even with all these wonderful benefits, soon after localizing a site, you might begin to wonder about extensibility. This article will help you apply extensibility features of ASP.NET to handle enterprise localization scenarios and improve your localization-development process. (23 printed pages)

Download the source code for this article.

Contents

Introduction
Where, Oh, Where Should My Resources Go?
The Resource-Provider Model
Building a Database Resource Provider
Accessing Resources from External Assemblies
Supporting Custom Localization Expressions
Conclusion
Acknowledgements
Additional Resources

Introduction

ASP.NET 2.0 unleashed a number of wonderful improvements for localizing Web applications. I wrote about these new features in the MSDN article "ASP.NET 2.0 Localization Features: A Fresh Approach to Localizing Web Applications."

After playing with these new localization features, you'll immediately notice the following:

  • Generating resources for each page is now a piece of cake from Microsoft Visual Studio 2005 by invoking the Generate Local Resources menu item in Page Design view.
  • Creating and consuming global resources is much simpler, thanks to a better resource editor and strongly typed access.
  • Mapping resource entries to control properties and content areas is quite elegant, using declarative localization expressions.
  • The ResourceManager no longer requires manual instantiation, because the ResXResourceProviderFactory coordinates retrieving resource entries from local or global resources, allocating the ResourceManager as needed.
  • Automatic detection of browser culture preferences and assignment of that culture to the request thread make it easier to respect user culture preferences, even for anonymous users.

Not surprisingly, even with all of these wonderful benefits, we often want more. Soon after localizing a site with these great features, you might begin to wonder about other things, such as:

  • How do I pull resources from an alternate location, such as a separate resource assembly or database source?
  • How do I manage hybrid environments that use some local and global resources, but also have alternate data sources?
  • How can I control the source of resources and continue to take advantage of the ASP.NET 2.0 resource-provider model, localization expressions, and other designer integration features?
  • How do I leverage existing localization features and available extensibility options, to better meet the needs of my development environment and localization process?

That's why extensibility is so very important. There are many ways to extend ASP.NET localization features and interact with the development environment. This article is the first in a three-part series that will help you apply extensibility features of ASP.NET to handle enterprise localization scenarios and improve your localization-development process.

In this article, I'll focus on features that enable you to retrieve resources from alternate storage locations and integrate with page parsing, compilation, and run-time execution. I'll describe how to achieve this using a combination of custom resource providers, custom expression builders, and other supporting extensible types. The second article in the series will show you how to improve your development process further by integrating your choice of resource storage with the built-in productivity features in Visual Studio 2005. The third article will address alternatives for handling complex resource hierarchies that can, for example, support client-side customization.

Where, Oh, Where Should My Resources Go?

Incorporating localized resources into a Web site has always been a painful endeavor. Generating resources has traditionally been difficult, organizing resources for translation will always require a managed process, but the more challenging aspect of resources in a Web site is knowing what should go into resources, how one should allocate those resources, and what will yield the best performance and maintainability.

Creating and Accessing Resources with ASP.NET 2.0

ASP.NET 2.0 provided us with a feature that would generate local resources for each page. From this, a more efficient page design and internationalization process was born.

  1. Design pages by applying a combination of static HTML and ASP.NET server controls.
  2. Prepare static areas for localization by wrapping them with the ASP.NET Localize control.
  3. Provide proper control names to all server controls so that generated event handlers and resource keys can be easily recognized.
  4. Create shared resources in the App_GlobalResources subdirectory. These can be .resx files that already exist, or new .resx files created to hold terms that will be shared across several pages.
  5. Associate shared resources with control properties using explicit resource expressions, where appropriate. It is best to do this before generating local resources for the page.
  6. Generate local resources while in Page Design view by selecting the Generate Local Resource menu item.

After generating local resources, all localizable properties for the page and its controls are pushed to individual local resource files, one per page. An implicit localization expression tells the page parser to generate code that will map each resource value for a control to its corresponding property, based on a common prefix. Consider the following implicit expression from the Expressions.aspx page in the sample code.

  <asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

Resources are stored in the Expressions.aspx.resx file beneath the App_LocalResources directory. Resources for this Label control share the prefix "labHelloLocalResource1"; for example, the Text property is stored by the "labHelloLocalResource1.Text" key.

If you factor your user interface well, using master pages and user controls for common user-interface regions, the resulting resources generated for each master page, user control, and page will also be somewhat well-factored (reduced overlap). This makes it easier to organize the resources consumed by each page part—something traditionally cumbersome in past versions. Still, sometimes you want to pull resources from a shared location. In this case, you would provide an explicit resource expression, such as the $Resources expression shown here.

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

In this case, resources are located in CommonTerms.resx beneath the App_GlobalResources directory. Explicit expressions like this can be created using the Expression Editor (see the MSDN article mentioned earlier), to simplify the process.

Both implicit and explicit expressions trigger code generation to retrieve resource values using the resource provider. These declarative expressions, combined with code and resource generation, supply a productivity tool we simply didn't have before—at least not for Web applications.

Resource Assemblies and the ResourceManager

There are several possible ways to compile and deploy your ASP.NET 2.0 applications:

  • Deploy source and JIT-compile the entire site.
  • Precompile the site with updatable pages and resources.
  • Precompile the site to generate an assembly per page, or an assembly per directory.

In all cases, resource assemblies are ultimately created for each directory in the site, and satellite assemblies are generated beneath their respective culture-specific directories. Even when the site is JIT-compiled, the outcome is equivalent. Figure 1 illustrates the precompiled result for a site with two subdirectories and a single translation to Spanish.

Click here for larger image

Figure 1. Resources and satellite assemblies are generated for each directory in an ASP.NET Web site. (Click on the picture for a larger image.)

These resources are accessed at run time through a ResourceManager. A ResourceManager is allocated for each resource type (for example, Page1.aspx and Page2.aspx) when resources are requested. Resource assemblies associated with each resource type are loaded into the ASP.NET application domain on first access, and remain there until the application domain is unloaded. In the case of Figure 1, the first time that \SubDir1\Page1.aspx is accessed for the Spanish culture, the assembly \es\App_LocalResources.subdir1.cdcab7d2.resources.dll is loaded into the application domain. This assembly contains Spanish resources for all pages in \SubDir1.

Figure 2 illustrates how the ResourceManager accesses local resources for a particular page. When \SubDir1\Page1.aspx is loaded, the code generated from implicit expressions will invoke the ResXResourceProviderFactory, which returns the LocalResXResourceProvider. This provider creates a ResourceManager for the Page1.aspx type in the App_LocalResources.subdir1.cdcab7d2 assembly. If the request thread has a UI culture of "es", the satellite resource assembly from the \es directory is loaded into the application domain. If the UI culture does not have a matching satellite assembly, the ResourceManager "falls back" to the main resource assembly.

Click here for larger image

Figure 2. The ResourceManager accesses resources from the main resource assembly or from localized satellite assemblies, after they have been loaded into the application domain. (Click on the picture for a larger image.)

Resource and satellite assemblies remain loaded in the application domain. Each ResourceManager (per page or shared resource type) is also cached and reused for subsequent requests to its associated resources.

The behavior I've just described summarizes how resources are accessed using the default ASP.NET resource-provider model. Now, let's talk about reasons for why you might deviate from this default implementation.

Why Use an Alternate Location?

With the new ASP.NET 2.0 experience, if you take the defaults for resource generation and run-time access, it yields a much nicer experience than in the past. That said, it is still often desirable to explore alternatives for resource storage, for the following reasons:

  • Reuse of existing resources, already located in alternate storage
  • Practicality for storing larger blocks of static content
  • Manageability

Two popular alternatives to resource storage are external resource assemblies and the database.

Dealing with pre-existing resources—You might have pre-existing resource assemblies from earlier applications, or resource assemblies that are shared between your Windows and Web applications. Normally, if you were migrating code from ASP.NET 1.1 to 2.0, I would recommend you take the .resx files from the 1.1 application and copy them to the App_GlobalResources directory of the 2.0 application. These .resx would then be compiled with the ASP.NET 2.0 application and accessed through strongly typed global resources. But, to control versioning on a pre-existing resource assembly, or to maintain only one copy of resources for both Windows and Web applications, this isn't an option. So, storing those resources in shared resource-only assemblies is a better option. That means that you need a way to pull resources from those assemblies.

Storing resources in the database—Database storage is a popular option for Web-application resources, for a number of reasons. You can probably guess, at some point with a site that has thousands of pages and multiple-thousand resource entries, using assembly resources might not be ideal. It will add to run-time memory usage, not to mention the increased number of assemblies loaded into the application domain. Both of these results can have a negative impact on performance for extremely large sites, making the latency of a database call worthwhile. Database resources might also provide a more flexible and manageable environment for the localization process, for reducing duplicates, for intricate caching options, and for storing possibly larger blocks of content. Lastly, allocating resources to a database makes it possible to support more complicated hierarchies of translated content, where customers or departments might have customized versions of the text that is then also localized.

Extensibility patterns for ASP.NET localization were specifically designed to support alternatives for resource storage, making these results easily attainable. Furthermore, you can also hook into the design-time experience, so that developers not only retrieve from but also generate resources in alternate stores—the latter to be covered in my next article.

Whether you are storing resources in external resource assemblies or in a database, you definitely want to leverage localization features of ASP.NET 2.0. The goal is to continue to use localization expressions and localization APIs, while accessing resources from "wherever." This is possible with the extensibility features that I'll discuss throughout the remainder of this article.

The Resource-Provider Model

As I've mentioned, the ResourceManager type is responsible for retrieving resources from assemblies at run time. It encapsulates the retrieval of the correct resource set, based on the request thread's UI culture (Figure 2). In other words, so long as the request thread is set to the correct UI culture for the calling user's preferences, the ResourceManager has all the logic to handle resource fallback, selecting the correct resource from the correct satellite assembly. Before ASP.NET 2.0, we had to write our own code to instantiate a ResourceManager for each resource type, and manage its lifetime. This required additional code for each page request to create or access the ResourceManager instance and invoke methods to access the resource entry. To bind resources to page elements in a declarative manner, custom data-binding statements could be used, but this also required code to initiate page-level data binding and the allocation of binding variables.

In ASP.NET 2.0, we can use localization API functions from any page or user control to retrieve resources. For example, the following code retrieves a local page resource and a global resource, respectively.

  this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

I mentioned earlier that declarative localization expressions could also be used to set page and control properties from resources. Implicit localization expressions, such as:

  <asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

and explicit localization expressions, such as:

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

are used to generate code to call GetLocalResourceObject() and GetGlobalResourceObject(). What that should tell you is that the ASP.NET 2.0 way to access resources is ultimately through these methods, even when you use the convenience of declarative expressions.

This is where the resource-provider model steps in. These API calls rely on a default or custom ResourceProviderFactory to find the correct resource entry and collect its value. The default ResourceProviderFactory is the ResXResourceProviderFactory type mentioned earlier. This factory returns an instance of the GlobalResXResourceProvider for global resources, and an instance of the LocalResXResourceProvider for local page resources.

Ultimately, these providers rely on a ResourceManager to access individual resources from the appropriate satellite assembly. The provider uses a ResourceReader to gather a collection of page resources during the page parsing step. Figure 3 illustrates these key components associated with the default resource-provider model.

Click here for larger image

Figure 3. Components that make up the default resource-provider model: the provider factory, local and global resource providers, resource managers, and resource readers to access each resource type (Click on the picture for a larger image)

This provider model has several benefits:

  1. It handles the activation and lifetime of each ResourceManager.
  2. Localization expressions and other resource APIs leverage the provider to find resources, thus increasing productivity through simplified and abstracted APIs.
  3. The provider model is extensible, making it possible to change where we store resources, while continuing to leverage productivity features of ASP.NET 2.0.

Now, I'll explore how you build a custom resource provider.

Building a Database Resource Provider

With a custom resource provider, you can access resources that do not originate from App_GlobalResources or App_LocalResources. For example, you can use a custom resource provider to access resources deployed in precompiled assemblies, or to access content from a database. In this section, I'll discuss the database resource-provider model, and later in this article I'll talk about accessing external resource assemblies.

A custom resource provider includes a ResourceProviderFactory and at least one resource-provider type that implements the IResourceProvider interface. The factory is responsible for instantiating the appropriate IResourceProvider to access local or global resources. Figure 4 illustrates the components that compose the database resource-provider model implementation in the sample code for this article.

Aa905797.exaspnet20rpm04(en-us,MSDN.10).gif

Figure 4. Component hierarchy for the custom database resource-provider model

Database Resource Entries

It might help first to review the structure of the database table that will store actual resource entries. The sample includes a SQL script to create a database named CustomResourceProvidersSample, with a table named StringResources. Table 1 includes the following fields:

Table 1. Database table with resource entries

Field Description
resourceType Category for each resource. It can be used to distinguish local resources for different pages, or global resource types by a user-defined name.
cultureCode Culture code from supported CultureInfo codes used by .NET, based on ISO standards. This can also be extended for any missing codes.
resourceKey Resource key used to retrieve resources.
resourceValue The resource value. This table supports strings up to 4K.

In this sample, all resources are stored in a single table, although in more complex or large-scale environments it is possible to distribute this among several tables to optimize for typical usage patterns. The primary key for the table is a composite key including resourceType, cultureCode, and resourceKey. Single resource values will typically be requested using the primary key. Figure 5 shows a partial view of the table's contents.

Click here for larger image

Figure 5. A partial view of the resource entries for this sample (Click on the picture for a larger image)

The resourceType for page resources is the page name, including its relative path in the application (that is, Expressions.aspx, SubDir1/Expressions.aspx). This convention will disambiguate pages of the same name belonging to different subdirectories—similar to how the default resource-provider model expects a different local resource assembly per subdirectory. Resource keys for control properties follow the same naming convention as typical page resources, using a control prefix and property name with the following syntax.

  [Prefix].[PropertyName]

Global resources have a user-defined resourceType. The sample code has several global resource categories: Glossary, CommonTerms, and Config. Resource keys in this case are named intuitively for their content.

The data-access layer, StringResourcesDALC, will abstract the work to retrieve resources from this table, based on usage patterns of the provider model.

Extending ResourceProviderFactory

The ResourceProviderFactory type is the hub for resource access in ASP.NET 2.0, responsible for returning a global or local resource provider depending on the requested resource type. ResourceProviderFactory is an abstract base type that requires an implementation for two methods: CreateLocalResourceProvider() and CreateGlobalResourceProvider(). To create a custom provider factory, you inherit this base type providing an implementation for these methods. Both methods must return an instance of a resource provider that implements the IResourceProvider interface.

The base ResourceProviderFactory type declaration is shown in Listing 1.

Listing 1. ResourceProviderFactory abstract type

  public abstract class ResourceProviderFactory
{
      protected ResourceProviderFactory();
      public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
      public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}

The ResourceProviderFactory supplies resource providers to the page-parsing step of compilation, and at run time for localization API calls.

  • Page Parser—The page is parsed at design time and as a precursor to page compilation. Explicit expressions for local and global resources are validated during this process. During compilation, code is generated in the compiled page for all expressions. Resource providers are used by the parser during this process.
  • Run Time—At run time, expressions no longer have meaning in compiled pages. Code generated during compilation uses the localization API to access local and global resources. A resource provider is created for local and global resource types.

In the sample code, the DBResourceProviderFactory creates a DBResourceProvider for both paths. That's because local and global resources are accessed the same way. The code for the DBResourceProviderFactory is shown in Listing 2.

Listing 2. DBResourceProviderFactory is a custom implementation of ResourceProviderFactory that supports database resources.

  using System;
using System.Web.Compilation;
using System.Web;
using System.Globalization;

namespace CustomResourceProviders
{
  public class DBResourceProviderFactory : ResourceProviderFactory
  {

    public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
    {
      return new DBResourceProvider(classKey);
    }

    public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
    {
      string classKey = virtualPath;
      if (!string.IsNullOrEmpty(virtualPath))
      {
        virtualPath = virtualPath.Remove(0, 1);
        classKey = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1);
      }
      return new DBResourceProvider(classKey);
    }
  }
}

For implicit expressions, or explicit expressions that invoke local resources, GetLocalResourceProvider() is called to create the provider for the page. The following is an example of an implicit expression and an explicit expression using local resources—as defined in the Expressions.aspx page from the code sample.

  <asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

GetLocalResourceProvider() takes a single parameter, the virtual path of the page including the application directory. Both of the above expressions will pass "/LocalizedWebSite/Expressions.aspx" to this parameter. From Figure 5, you can see that local resources are stored using a resourceType that represents the relative path of the page, without the application directory. Thus, GetLocalResourceProvider() strips the application directory from the path prior to creating an instance of the DBResourceProvider.

For explicit expressions that request global resources, the resource type specified directly in the expression is passed to GetGlobalResourceProvider(). Consider the following explicit expression (also from Expressions.aspx in the code sample).

  <asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

The resource type in this case is CommonTerms, so GetGlobalResourceProvider() is called passing CommonTerms as the parameter. An instance of DBResourceProvider is created for this type.

For any given resource type, only one instance of DBResourceProvider is created. After it is created, it is cached for future use. So, the factory is called only if the provider instance does not yet exist in the cache. The process of creating and caching the provider is encapsulated in the localization API used to access resources.

ResourceProviderFactory Configuration

The runtime will use the ResxResourceProviderFactory, unless you specify an alternate ResourceProviderFactory type in configuration. The <globalization> section of the Web configuration file has an attribute named resourceProviderFactoryType. Here, you specify the ResourceProviderFactory type that should be used. To configure the DBResourceProviderFactory, you would add the following setting.

  <system.web>

...other settings

      <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.DBResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
</system.web>

Note   In the sample code provided, the DBResourceProviderFactory belongs to the CustomResourceProviders namespace, in the CustomResourceProviders assembly. This assembly is strongly named and can be installed in the global assembly cache (GAC).

Now, the DBResourceProviderFactory will be used to create resource providers during page parsing and at run time.

Implementing IResourceProvider

At the heart of the resource-provider model is the resource-provider type. Although the ResourceProviderFactory is an important abstraction, resource providers are ultimately responsible for returning resource entries at run time—from wherever they are stored. As discussed in the previous section, providers are created by the ResourceProviderFactory implementation and then cached for future use. From the database resource-provider model shown in Figure 4, you can see that the DBResourceProvider type services both local and global resources. This type is responsible for retrieving resources from the database, but it leverages the DBResourceReader and StringResourcesDALC components to handle this task.

Resource providers implement the IResourceProvider interface, shown in Listing 3.

Listing 3. IResourceProvider interface

  public interface IResourceProvider
{
      object GetObject(string resourceKey, CultureInfo culture);
      IResourceReader ResourceReader { get; }
}

Individual resources are retrieved through GetObject(), and the ResourceReader property should return a collection of resources based on the resource type of the provider instance.

During the page-parsing step, providers are used to retrieve all local resources for a page; explicit expressions are validated; and, during compilation, code is also generated for the page. For local resources, the resource reader is used to generate code for implicit expressions. Explicit expressions for local and global resources are validated individually with a call to GetObject() on the appropriate provider.

At run time, the parser-generated code triggers calls to GetObject() to retrieve local and global resources as the page is initialized.

Retrieving Individual Database Resources

The DBResourceProvider implementation for GetObject() is the following.

  public object GetObject(string resourceKey, CultureInfo culture)
{

  if (string.IsNullOrEmpty(resourceKey))
  {
    throw new ArgumentNullException("resourceKey");
  }  
  
  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }
            
  string resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);
}

In fact, the work to retrieve resources is delegated to the StringResourcesDALC type (see Figure 4) to handle database queries. This component isolates the provider from resource fallback and other logic required to find the actual resource.

GetResourceByCultureAndKey() initializes the database connection and executes a SqlDataReader to retrieve values, including the required resource fallback logic (to be discussed later).

Retrieving Resources in Batch

DBResourceProvider returns an instance of DBResourceReader in the following implementation of the ResourceReader property.

  public System.Resources.IResourceReader ResourceReader
{
  get
  {
    ListDictionary resourceDictionary = this.m_dalc.GetResourcesByCulture(CultureInfo.InvariantCulture);

    return new DBResourceReader(resourceDictionary);
  }
}

StringResourcesDALC is responsible for gathering the default resources for a particular type (InvariantCulture). The ListDictionary created from query results is wrapped by the DBResourceReader for enumeration.

DBResourceReader implements IResourceReader. Key elements of this implementation are shown here.

  public class DBResourceReader : DisposableBaseType, IResourceReader, IEnumerable<KeyValuePair<string, object>>
{
  private ListDictionary m_resourceDictionary;
  public DBResourceReader(ListDictionary resourceDictionary)
  {
    this.m_resourceDictionary = resourceDictionary;
  }

  public IDictionaryEnumerator GetEnumerator()
  {
    return this.m_resourceDictionary.GetEnumerator();
  }

  // other methods

}

The page parser uses the reader's dictionary enumerator to generate code for implicit expressions. If a reader isn't supplied, or the reader holds an empty dictionary, the code cannot be generated. Implicit expressions do not require that a value be present for every property value, because it is not explicit. So, for implicit expressions, default property values are rendered with the page if no code is generated to set the value.

Resource Fallback

Resource fallback is an important part of the resource-provider implementation. Resources are requested at run time, based on the current UI culture for the request thread.

System.Threading.Thread.Current.CurrentUICulture

If that culture is a specific culture such as "es-EC" or "es-ES", the resource provider should look to see if a resource exists for that specific culture. It is possible, however, that the resources were only specified for the neutral culture, "es". The neutral culture is the parent. If any specific entries cannot be found, the parent is checked next. Ultimately, the default culture for the application should be used, so that a value is found. In this sample, the default culture is "en".

Resource fallback is encapsulated by the data-access component, StringResourcesDALC. When a call is made to retrieve a resource, GetResourceByCultureAndKey() is invoked. This function is responsible for opening a database connection, calling a recursive function that will do resource fallback, and subsequently closing the database connection. The implementation for GetResourceByCultureAndKey() is shown here.

  public string GetResourceByCultureAndKey(CultureInfo culture, string resourceKey)
{
  string resourceValue = string.Empty;

  try
  {
    if (culture == null || culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }

    this.m_connection.Open();
    resourceValue = this.GetResourceByCultureAndKeyInternal
(culture, resourceKey);
  }
  finally
  {
    this.m_connection.Close();
  }
  return resourceValue;
}

The recursive function, GetResourceByCultureAndKeyInternal(), will first attempt to find the resource by the specified culture. If that cannot be found, the parent culture is sought and the query is retried. If that fails, the default culture is used in a final attempt to find a resource entry. When a resource entry is not found for the default culture, this is considered a fatal exception in this sample. The listing for GetResourceByCultureAndKeyInternal() is shown here.

  private string GetResourceByCultureAndKeyInternal
(CultureInfo culture, string resourceKey)
{

  StringCollection resources = new StringCollection();
  string resourceValue = null;

  this.m_cmdGetResourceByCultureAndKey.Parameters["cultureCode"].Value 
= culture.Name;
                
  this.m_cmdGetResourceByCultureAndKey.Parameters["resourceKey"].Value 
= resourceKey;

  using (SqlDataReader reader = this.m_cmdGetResourceByCultureAndKey.ExecuteReader())
  {
    while (reader.Read())
    {
      resources.Add(reader.GetString(reader.GetOrdinal("resourceValue")));
    }
  }

  if (resources.Count == 0)
  {
    if (culture.Name == this.m_defaultResourceCulture)
    {
      throw new InvalidOperationException(String.Format(
Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DefaultResourceNotFound, resourceKey));
    }

    culture = culture.Parent;
    if (culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }
    resourceValue = this.GetResourceByCultureAndKeyInternal(culture, resourceKey);
  }
  else if (resources.Count == 1)
  {
    resourceValue = resources[0];
  }
  else
  {
    throw new DataException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DuplicateResourceFound, resourceKey));
  }

  return resourceValue;
}

As an alternative, resource fallback could also be encapsulated in a stored procedure or SQL CLR component—because the rules for fallback are likely to be coupled to the database design to some extent, thus aren't necessarily important to the business tier.

Resource Caching

With the default provider model, when resources are drawn from resource assemblies, the assemblies are loaded once and cached in the application domain. For database resources, we have to implement our own caching mechanism to avoid hitting the database for every single request for a resource. The DBResourceProvider handles this task.

Earlier, I demonstrated what the GetObject() implementation looked like for the provider, without caching. Resources are retrieved from the database with a call to the data-access layer, as follows.

    resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

Remember that a single provider instance exists per resource type, and is cached for repeated use. Inside the provider, if we cache resource entries in a dictionary for each culture requested, those dictionary entries will be cached with the provider in memory. The code to retrieve an object can first look to the dictionary cache for the value, and, if it is not found, create a cached entry after retrieving it from the database. The result is shown here.

  string resourceValue = null;
Dictionary<string, string> resCacheByCulture = null;
if (m_resourceCache.ContainsKey(culture.Name))
{
  resCacheByCulture = m_resourceCache[culture.Name];
  if (resCacheByCulture.ContainsKey(resourceKey))
  {
    resourceValue = resCacheByCulture[resourceKey];
  }
}

if (resourceValue == null)
{
  resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

  lock(this)
  {
    if (resCacheByCulture == null)
    {
      resCacheByCulture = new Dictionary<string, string>();
      m_resourceCache.Add(culture.Name, resCacheByCulture);
    }
  resCacheByCulture.Add(resourceKey, resourceValue);
  }
}

return resourceValue;

Caching is a necessary part of performance when storing resources in the database. In this example, the values will be cached until the application domain is freed, which means dynamic updates to resources in the database will not be reflected at run time unless you restart the application. To allow for this type of dynamic update, additional work to cache resources with a database cache dependency is required.

Thread Safety

Another concern we have in a Web environment is thread safety. The components participating in the database provider model from Figure 4 are designed to be thread-safe using .NET synchronization techniques.

An instance of the DBResourceProvider or StringResourcesDALC for a particular resource type can be invoked by multiple threads—simply, by two requests for the same page. In the StringResourcesDALC component, public methods that retrieve data from the database modify instance variables for the type, including opening the connection, setting query-parameter values, and executing SqlDataReader. To make these functions thread-safe, the MethodImplAttribute has been applied.

  [MethodImpl(MethodImplOptions.Synchronized)]

This attribute locks the StringResourcesDALC object for the duration of the method call, blocking other callers. If resources are cached, the data-access component is not invoked, which enhances performance.

In the DBResourceProvider, modifications to the dictionary cache are also locked with a classic lock statement.

    lock(this)
   { ... }

The expanded view of this caching code is shown in the previous section. The lock statement locks the entire object and its members for the duration of the code block. That means that only one thread can add values to the cache at a time.

Using the Custom Resource Provider

In the past, we used to code manually the instantiation and lifetime management of a ResourceManager for each resource type. With ASP.NET 2.0, the resource-provider model handles this for us, creating and caching resource providers on demand, so long as we program against the localization APIs. That means that we should use ASP.NET 2.0 techniques to access resources with:

  • Page-object methods.
  • HttpContext methods.
  • Localization expressions.

Master pages, Web pages, and user controls all share a common base type, TemplateControl. This base type provides the two overloaded operations for resource access that I mentioned earlier: GetLocalResourceObject() and GetGlobalResourceObject(). These operations use cached resource providers to retrieve resources through the provider's GetObject() implementation. If the provider has not yet been cached, the ResourceProviderFactory is used to create it using CreateLocalResourceProvider() or CreateGlobalResourceProvider(). The advantage of this approach is that you can conveniently write page code to retrieve resource values.

  this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

In fact, very similar code is generated for each page from implicit and explicit expressions during compilation.

You can also access local and global resources through static methods on the HttpContext type—useful for writing code that is not part of a page.

  this.labHelloLocal.Text = HttpContext.GetLocalResourceObject("/RuntimeCode.aspx", "labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = HttpContext.GetGlobalResourceObject("CommonTerms", "Hello") as string;

In actuality, localization expressions are a much more convenient way to access resources. Localization expressions provide a declarative model that automatically generates code to access resources through the localization API. Thus, all paths lead to the localization API, and to the configured ResourceProviderFactory.

Database Resources: The Pros and Cons

Moving resources to the database yields a number of benefits, including the following:

  • You can introduce complex hierarchical requirements for resources without impact to the calling code—for example, allowing customization of default strings by customer or department, while still allowing each of them to translate those strings.
  • Larger chunks of HTML content can be managed more easily at the database tier for reasons of content organization and more flexibility with caching and memory usage (by default, remember, satellite resources are loaded into the application domain with all their embedded content, whereas database resources can be cached, or freed from the cache using a more fine-tuned algorithm).
  • Storing information in a single place—the database—in lieu of many .resx files, can improve overall manageability of a localized application. It can also simplify your approach for working with translators.

There are also a few downsides to database storage:

  • It definitely requires additional forethought and planning. How will resources be organized into tables? Should they all go into a single table? Should they be organized by category? Should they be accessed through a single stored procedure or SQL CLR component, to provide later distribution among tables?
  • You have to do some work to integrate the productivity features of Visual Studio 2005 with your database resources. That is, you won't automatically generate resources in the database with Generate Local Resources, or view database information in the Expression Dialog, among other things. This level of integration requires you to build custom components that integrate with the design-time experience for developers—something I'll discuss in my next article.

Despite the work required to integrate productivity features with database resources, one could argue that the benefits outweigh these issues. Furthermore, being forced to plan the structure and organization of resources is something we "should" be doing, even for the default resource-allocation structure!

Accessing Resources from External Assemblies

You can also use the resource-provider model to access resources from precompiled, external assemblies. This makes it possible to share common resources among Web and Windows applications, while providing a single unit for versioning and deployment. In this section, I'll explain how you can apply the concepts just discussed for accessing this type of external resource assembly.

Figure 6 shows the components that make up this external resource-provider model.

Click here for larger image

Figure 6. Component hierarchy for the external resource-provider model (Click on the picture for a larger image)

You'll notice a few things about this implementation:

  • Only global resources are supported. It wouldn't make sense to replace the page resource model provided for free with ASP.NET 2.0. Only global resources will be drawn from external resource assemblies.
  • We cannot access the LocalResXResourceProvider from the ExternalResourceProviderFactory. This is an internal type that is not available for construction from our code. If we replace the default provider with the ExternalResourceProviderFactory, only global resources will be supported (I'll discuss an alternative to this in a later section).
  • The ResourceManager is used to access resources. The default ResourceManager already gives us a way to access resources from assemblies, so we don't need to replace this functionality to access external resources.

Now, I'll show you the highlights of this implementation.

Same Localization Expressions, Different Use Case

Remember that resource providers are invoked because of localization expressions and the localization API. To access external resources, explicit expressions will be used. These expressions will be similar to those used to access global resources, with a few minor changes; specifically, the assembly name must be supplied along with the resource type. The default provider knew how to find the global resource assembly. This external resource provider relies on the assembly name to achieve the same result.

The syntax for a $Resources expression for the default provider model (explicit global resources) is the following.

  <%$ Resources: [resourceType], [resourceKey] %>

The same expression can be used to access external resources when the ExternalResourceProviderFactory is configured, with the following syntax change.

  <%$ Resources: [assemblyName]|[resourceType], [resourceKey] %>

For example, to retrieve a resource from the CommonResources.dll assembly, from the global resource type "CommonTerms", you would use the following explicit expression.

  <asp:Label ID="labGlobalResource" runat="server" Text="<%$ Resources:CommonResources|CommonTerms, Hello %>" ></asp:Label>

This generates the following code, when the page is compiled.

  labGlobalResource.Text = this.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello");

This illustrates that the external resource-provider model can leverage existing expressions and code from the localization API, so long as the right information is provided. It will be the ExternalResourceProvider that ultimately parses the information to separate assembly name from resource type.

ExternalResourceProviderFactory

Like the DBResourceProviderFactory, ExternalResourceProviderFactory inherits ResourceProviderFactory and has overrides for CreateGlobalResourceProvider() and CreateLocalResourceProvider(). Listing 4 shows the complete implementation.

Listing 4. Implementation of ExternalResourceProviderFactory

  public class ExternalResourceProviderFactory : ResourceProviderFactory
{

  public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
  {
    return new GlobalExternalResourceProvider(classKey);
  }

  public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
  {
    throw new NotSupportedException(String.Format
(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_LocalResourcesNotSupported, "ExternalResourceProviderFactory"));
  }
}

CreateGlobalResourceProvider() is responsible for instantiating the GlobalExternalResourceProvider type with the class key provided. Remember that the class key for this provider must include the assembly name and the resource type. CreateLocalResourceProvider() throws a NotSupportedException, because we aren't storing local resources in external assemblies. This will actually cause a parsing exception, if you use local expressions on the page. So, if you want to continue to support local resources, this might not be the ideal scenario for hooking the ExternalResourceProvider. Later, I'll show you how to avoid this problem with custom localization expressions.

To hook up the ExternalResourceProviderFactory to existing expressions and the localization API, we return to the <globalization> section of the Web.config.

  <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.ExternalResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />

Now, the default provider model is replaced with the external resource-provider model. Let's see how the provider works.

GlobalExternalResourceProvider

GlobalExternalResourceProvider implements IResourceProvider. This provider is very similar to the GlobalResXResourceProvider, with one exception: This provider retrieves global resources from pre-existing satellite assemblies, and requires knowledge of a specific assembly name where resources are stored.

The constructor of the GlobalExternalResourceProvider receives an assembly name and resource type separated by the piping symbol ("|"). This information is parsed, as shown here.

  public GlobalExternalResourceProvider(string classKey)
{
  if (classKey.IndexOf('|') > 0)
  {
    string[] textArray = classKey.Split('|');
    this.m_assemblyName = textArray[0];
    this.m_classKey = textArray[1];
  }
  else
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_InvalidConstructor, classKey));

}

If the format of the classKey parameter passed to the constructor is invalid, an ArgumentException is thrown. This causes the page parser to report an error for explicit expressions. Code written directly against the localization API will fail at run time.

A provider instance is created and cached for each unique assembly and resource type combination. When a resource is requested during page parsing (for validation) or at run time, GetObject() is called, as shown here.

  public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
  this.EnsureResourceManager();
  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }
  return this.m_resourceManager.GetObject(resourceKey, culture);
}

Internally, the GlobalExternalResourceProvider relies on the existing functionality of the ResourceManager type to retrieve resources and handle resource fallback. The trick is to create a ResourceManager for the right assembly. The first time that EnsureResourceManager() is called, it loads the resource assembly and creates an instance of the ResourceManager for the specified type within that assembly. An exception will occur if you specify an assembly that doesn't contain the resource type. The code to load the assembly and create the ResourceManager is shown here.

  Assembly asm = Assembly.Load(this.m_assemblyName);
ResourceManager rm = new ResourceManager(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", this.m_assemblyName, this.m_classKey), asm);
this.m_resourceManager = rm;

Using the ExternalResourceProvider, you can retrieve resources from any assembly deployed to the \bin directory of the Web application, or from the Global Assembly Cache (GAC).

The provider returns a NotSupportedException for the ResourceReader property, because local resources aren't supported; thus, implicit localization expressions will not be parsed.

Supporting Custom Localization Expressions

Configuring a custom provider is great for situations in which all resources will be stored in an alternate location, and you don't plan to leverage resources located in App_LocalResources and App_GlobalResources, respectively. What if you want to support the standard implementation for local and global resources (default provider), while also having the option to pull some resources from another source (custom provider)? You can achieve this by implementing custom expressions that target the custom resource provider.

How ResourceExpressionBuilder Works

Expressions are processed by expression builders that interact with the page-parsing step, prior to compilation. Expressions include anything delimited with <%$ %>—including application settings, connection strings, and localization expressions. The syntax for such expressions is shown here.

  <%$ [prefix]: [declaration] %>

As you know, localization expressions use the prefix "Resources". The page parser uses the ResourceExpressionBuilder type to process those expressions. That's because the ResourceExpressionBuilder has been mapped to the prefix "Resources" for the run-time default of the <expressionBuilders> configuration.

  <expressionBuilders>
<add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
</expressionBuilders>

For compiled pages, here is how it works:

  • When a page is parsed, the expression builder's ParseExpression operation is called to validate the syntax of the expression. If the expression is invalid (for example, the specified resource cannot be found), a parsing error is generated.
  • If parsing is successful, the expression builder's GetCodeExpression operation is called to request the code to generate for the expression. This is where the expression builder emits code for page initialization. The code is injected into the compiled IL for the page.

When page compilation is disabled, expressions are evaluated in a slightly different way. You can disable page compilation for individual pages.

  <%@ Page Language="C#" CompilationMode="Never" %>

Or you can disable compilation for all pages in the Web.config file.

  <pages compilationMode="Never" />

In this case, when the page is requested, it is parsed; during the parse step, the expression builder's SupportsEvaluate property is checked to see if the page can be processed without compilation. Code is not generated for the page.

At run time, SupportsEvaluate is once again verified, followed by a call to EvaluateExpression to retrieve a value for each localization expression.

ResourceExpressionBuilder is derived from ExpressionBuilder. ExpressionBuilder is the common base type that exposes abstract and virtual methods—implemented by ResourceExpressionBuilder to support page parsing, code generation, and expression evaluation. So, to support custom localization expressions, you can extend ExpressionBuilder and provide your own implementation.

Extending ExpressionBuilder

To support custom localization expressions, you need a custom expression-builder implementation. Like the ResourceExpressionBuilder, you can extend ExpressionBuilder and provide a custom implementation for page parsing, code generation, and expression evaluation for non-compiled pages.

First, let's review the purpose for the custom expression builder in this sample, and the expected syntax for its implementation. The goal is to leave the default implementation for <%$ Resources %> intact, while supporting resources that are drawn from an external assembly. To achieve this, instead of replacing the resource provider altogether, we'll create a new expression to handle it. That means that we need a new expression prefix, a custom ExpressionBuilder, and a way to associate this new prefix with the custom ExpressionBuilder.

For this example, the new prefix is "ExternalResource". The required syntax for this new expression is shown here.

  <%$ ExternalResource: [assemblyName]|[resourceType], [resourceKey] %>

This expression will draw resources from a specified assembly using the same GlobalExternalResourceProviderdiscussed earlier. To support this new expression, we'll create a custom type, ExternalResourceExpressionBuilder.Table 2 summarizes the functionality to be provided by each of the overridden ExpressionBuilder methods.

Table 2. Summary of functionality provided by each overridden method

Method Description
EvaluateExpression Returns the resource value for an ExternalResource expression in non-compiled pages.
GetCodeExpression Returns the code to be emitted for an ExternalResource expression. This code will invoke the custom resource provider, GlobalExternalResourceProvider.
ParseExpression Validates an ExternalResource expression by attempting to access resources for the expression. Page parsing will fail if the resource cannot be found.
SupportsEvaluate property Indicates if non-compiled page evaluation is supported. In this implementation, returns true.

Using the ExternalResourceExpressionBuilder, custom localization expressions such as the following can be declared.

  <asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

Remember that expressions are parsed at design time, and prior to compilation. ParseExpression is called during page parsing to verify that the resource expression is accurate and that the requested resource actually exists. The following code illustrates this implementation.

  public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
{
  if (string.IsNullOrEmpty(expression))
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture,Properties.Resources.Expression_TooFewParameters, expression));
  }

  ExternalResourceExpressionFields fields = null;
  string classKey = null;
  string resourceKey = null;
            
  string[] expParams = expression.Split(new char[] { ',' });
  if (expParams.Length > 2)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooManyParameters, expression));
  }
  if (expParams.Length == 1)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooFewParameters, expression));
  }
  else
  {
    classKey = expParams[0].Trim();
    resourceKey = expParams[1].Trim();
  }

  fields = new ExternalResourceExpressionFields(classKey, resourceKey);

              
  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider rp = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);
            
  object res = rp.GetObject(fields.ResourceKey, CultureInfo.InvariantCulture);
  if (res == null)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_ResourceNotFound, fields.ResourceKey));
  }
  return fields;
}

Most of the code is focused on validating the expression, but at the heart of it is the creation of the GlobalExternalResourceProvider, as well as a call to GetObject() to retrieve the resource.

When pages are compiled, page parsing is followed by code generation. At this time, the expression builder's GetCodeExpression implementation is called. This operation returns the code necessary to retrieve the resource value at run time, as shown here.

  public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

 CodeMethodInvokeExpression exp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(ExternalResourceExpressionBuilder)), "GetGlobalResourceObject", new CodePrimitiveExpression(fields.ClassKey), new CodePrimitiveExpression(fields.ResourceKey));

 return exp;
}

The output from GetCodeExpression results in generated code that is similar to the code in boldface, shown here.

  labExternalResource.Text = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

You'll notice that the generated code relies on a static method implemented by the ExternalResourceExpressionBuilder. GetGlobalResourceObject is a helper method that instantiates the GlobalExternalResourceProvider and retrieves the resource entry. For compiled pages, this code retrieves values from external resources at run time.

For non-compiled pages, expressions are evaluated at run time with a call to EvaluateExpression. ExternalResourceExpressionBuilder implements an override for EvaluateExpression that once again uses the GlobalExternalResourceProvider to retrieve the appropriate resource.

  public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider provider = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);

  return provider.GetObject(fields.ResourceKey, null);
}

After configuring the custom expression builder, you can freely include declarative statements to retrieve resources from external assemblies, while default localization expressions are still used to retrieve values from App_LocalResources or App_GlobalResources.

ExpressionBuilder Configuration

To configure a custom expression builder, you add it to the <expressionBuilders> section in the Web.config. In this example, we associate the ExternalResourceExpressionBuilder to the "ExternalResources" prefix, with this configuration.

  <expressionBuilders>
  <add expressionPrefix="ExternalResources" type="CustomResourceProviders.ExternalResourceExpressionBuilder, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1"/>
</expressionBuilders>

Now, all resource expressions that use the "ExternalResources" prefix will be parsed or evaluated according to the ExternalResourceExpressionBuilder implementation discussed in the previous section.

Accessing Local, Global, and External Resources

Listing 5 illustrates the application of all three localization expressions (implicit, explicit, and custom explicit) to pull resources from default and custom sources.

Listing 5. Implicit, explicit, and custom-explicit localization expressions in a single page

  <asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

Using a custom localization expression for external resources instead of configuring a replacement resource provider allows you to use local resources for each page, and global resources that are compiled with the Web site, while still having the flexibility to opt-in resources from external assemblies (or elsewhere). Using the ExternalResourceExpressionBuilder, you can also access external resources directly from code, using the static helper method mentioned earlier, GetGlobalResourceObject().

  string s = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

Using this technique, you don't have to replace the default resource provider with your own provider. Instead, you'll rely on code generation from custom localization expressions to pull resources from external assemblies on demand.

Conclusion

In this article, you have learned how to create a custom resource-provider model for accessing resources from the database, or from external resource assemblies. You also learned how to create custom localization expressions to incorporate alternate resource storage with the default provider model. By using the extensibility features of ASP.NET 2.0, you have some very accessible alternatives for resource allocation and retrieval. And the best part about it is how easy you can hook into the natural ASP.NET 2.0 programming model using declarative localization expressions.

The next article in this series will explore the other half of this picture: how to hook into the design-time experience for building resource expressions and generating resources in the appropriate storage location.

Acknowledgements

Many thanks to Simon Calvert and Eilon Lipton of Microsoft Corporation, who gave invaluable support and feedback on this article to ensure that it elaborated on the many use cases through the provider model.

Additional Resources

Michèle's blog: www.dasblonde.net (RSS on Globalization)

IDesign Inc

 

About the author

Michèle Leroux Bustamante is Chief Architect of IDesign Inc., Microsoft Regional Director for San Diego, Microsoft MVP for XML Web Services, and BEA Technical Director. At IDesign, Michèle provides training, mentoring, and high-end architecture consulting services, focusing on Web services, scalable and secure architecture design for .NET, interoperability, and globalization architecture. She is a member of the International .NET Speakers Association (INETA), a frequent conference presenter, and conference chair of SD's Web Services track, and is frequently published in several major technology journals.

Michèle is also on the board of directors for the International Association of Software Architects (IASA) and a Program Advisor to UCSD Extension. Her book on the Microsoft Windows Communication Foundation (WCF), published by O'Reilly, is scheduled for release in late 2006 (book blog: www.thatindigogirl.com). Reach her at mlb@idesign.net, or visit www.idesign.net and www.dasblonde.net.

© Microsoft Corporation. All rights reserved.