Click here to Skip to main content
15,895,709 members
Articles / Web Development / ASP.NET
Tip/Trick

Using C# Dynamics to Fetch Globalization RESX Strings via Webapi2

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
8 Nov 2014MIT2 min read 10.9K   7  
This tip shows how to make a reusable resource webapi using RESX as the backing System of Record.

Introduction

I have a plugin architecture where assemblies delivered in the form of nuget packages are consumed and loaded at runtime. So I don't know upfront what ResX assemblies may be present in my AppDomain. I wanted a standard Webapi to fetch any resource set and leverage all the good stuff that is already in place by following Microsoft's globalization design. I also realize that clients have differing needs of what form the data should take, and I want that to be extensible.

Background

I am using David Ebbo's StaticMembersDynamicWrapper implementation for calling static methods.
http://blogs.msdn.com/b/davidebb/archive/2009/10/23/using-c-dynamic-to-call-static-members.aspx

This is a Webapi2 implementation and things like antiforgery, exception handling are handled using Autofac's IAutofacActionFilter. The API code is free to and will throw exceptions, and my filters will catch and prettify the response. I can apply AntiForgery filters against this controller and the requests will never make it in. You can avoid this and get the yellow screen of death.

I set the culture using Application_BeginRequest, so the code I will write is guaranteed to have it in place.

C#
Thread.CurrentThread.CurrentUICulture = cultureInfo;
Thread.CurrentThread.CurrentCulture = cultureInfo;

Client

Clients of this service want an API to get the following:

  1. The ResourceSet that is requested, and
  2. In the right format, which is our Treatment.

Both the asks are in the form of a C# Type full name. You should be familiar with what a full C# type name looks like:

[full namespaced path to Class], [name of assembly]

i.e. COA.MEF.Calculator.Globalization.Views.Home, COA.MEF.Calculator.Globalization

In the example above, we are referring to the Home class Type, which is a result of a resx called Home.resx, and in which assembly it lives. You can refer to any class type this way, and that is how our treatment types are referred to as well.

On the backend, the data is in the form of a ResourceSet, it's basically a dictionary of strings.

A client, for whatever reason, wants that data to come down in a specific form of JSON, i.e., Angular Translate has a spec of what a translated resource set should look like.

Below are a couple of treatment example results that I coded up:

JavaScript
{
"value":[
{
    "Key":"OurMission",
    "Value":"Lorem Ipsum is simply dummy text of the printing and 
    typesetting industry. Lorem Ipsum has been the industry's standard dummy text 
    ever since the 1500s, when an unknown printer took a galley of type and 
    scrambled it to make a type specimen book. It has survived not only five centuries, 
    ut also the leap into electronic typesetting, remaining essentially unchanged. 
    It was popularised in the 1960s with the release of Letraset sheets containing 
    Lorem Ipsum passages, and more recently with desktop publishing software 
    like Aldus PageMaker including versions of Lorem Ipsum."
},
{"Key":"OurDog","Value":"Shelby"}
]
}

{
"value":{
    "OurMission":"Lorem Ipsum is simply dummy text of the printing 
    and typesetting industry. Lorem Ipsum has been the industry's standard dummy text 
    ever since the 1500s, when an unknown printer took a galley of type and scrambled 
    it to make a type specimen book. It has survived not only five centuries, but also 
    the leap into electronic typesetting, remaining essentially unchanged. 
    It was popularised in the 1960s with the release of Letraset sheets containing 
    Lorem Ipsum passages, and more recently with desktop publishing software 
    like Aldus PageMaker including versions of Lorem Ipsum.",
    "OurDog":"Shelby"
    }
}

Writing your own should be easy, and it is.

The API

[domain]/ResourceApi/Resource/ByDynamic?id=
[enc:C# Full type name]&treatment=[enc:C# Full type name]

http://localhost:46391/ResourceApi/Resource/ByDynamic?
id=COA.MEF.Calculator.Globalization.Views.Home%2C+COA.MEF.Calculator.Globalization&
treatment=Pingo.Contrib.Globalization.Treatment.KeyValueArray%2C+Pingo.Contrib.Globalization

The Code

Treatment
C#
public class StringResourceSet
{
   public string Key { get; set; }
   public string Value { get; set; }
}
 
public static class KeyValueArray
{
    public static object Process(ResourceSet resourceSet)
    {
        var result = (
          from DictionaryEntry entry in resourceSet
          select new StringResourceSet { Key = entry.Key.ToString(), Value = entry.Value.ToString() })
          .ToList();

        return result;
    }
}

public static class KeyValueObject
{
    public static object Process(ResourceSet resourceSet)
    {
        var expando = new System.Dynamic.ExpandoObject();
        var expandoMap = expando as IDictionary<string, object>;
        foreach (DictionaryEntry rs in resourceSet)
        {
            expandoMap[rs.Key.ToString()] = rs.Value.ToString();
        }
        return expando;
    }
}
Controller
C#
public static class ResourceApiExtensions
{
    public static int GetSequenceHashCode<T>(this IEnumerable<T> sequence)
    {
        return sequence
            .Select(item => item.GetHashCode())
            .Aggregate((total, nextCode) => total ^ nextCode);
    }
}

[RoutePrefix("ResourceApi/Resource")]
public class ResourceApiController : ApiController
{
    private HttpContextBase _httpContext;
    public ResourceApiController(HttpContextBase httpContext)
    {
        _httpContext = httpContext;
    }
    private static object InternalGetResourceSet(string id, string treatment)
    {
        var resourceType = Type.GetType(id);
        dynamic resourceTypeDynamic = new StaticMembersDynamicWrapper(resourceType);
        ResourceSet rs = resourceTypeDynamic.ResourceManager.GetResourceSet
            (CultureInfo.CurrentUICulture, true, true);

        var typeTreatment = Type.GetType(treatment);
        dynamic typeTreatmenteDynamic = new StaticMembersDynamicWrapper(typeTreatment);

        var value = typeTreatmenteDynamic.Process(rs);
        return value;
    }

    private static readonly CacheItemPolicy InfiniteCacheItemPolicy = 
        new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.AddYears(1) };

    // GET /ResourceApi/Resource/ByDynamic
    // ?id=<urlencode>COA.MEF.Calculator.Globalization.Views.Home, COA.MEF.Calculator.Globalization</>
    // &treatment=<urlencode>
    // Pingo.Contrib.Globalization.Treatment.KeyValueObject, Pingo.Contrib.Globalization</>

    //?id=COA.MEF.Calculator.Globalization.Views.Home%2C+COA.MEF.Calculator.Globalization&
    //treatment=Pingo.Contrib.Globalization.Treatment.KeyValueObject%2C+Pingo.Contrib.Globalization

    [Route("ByDynamic")]
    [ResponseType(typeof(object))]
    public async Task<IHttpActionResult> GetResourceSet(string id, string treatment)
    {
        var cache = MemoryCache.Default;
        var currentCulture = Thread.CurrentThread.CurrentCulture;
        var key = new List<object> 
            { currentCulture, id, treatment }.AsReadOnly().GetSequenceHashCode();

        var newValue = new Lazy<object>(() => 
            { return InternalGetResourceSet(id, treatment); });
        var value =
            (Lazy<object>)
                cache.AddOrGetExisting(key.ToString
                    (CultureInfo.InvariantCulture), newValue, InfiniteCacheItemPolicy);

        // as per documentation, AddOrGetExisting will return null first time
        //If a cache entry with the same key exists, the existing cache entry; otherwise, null.

        var result = value != null ? value.Value : newValue.Value;

        return Ok(result);
    }
} 

Resource Files (.ResX)

When you create your resource files (.ResX), you will notice that the "Custom Tool" is set to ResXFileCodeGenerator. Examining your {resource}.Designer.cs files that were produced by this tool, you will notice that everything is set to "internal". The code needs this set to "public".

Instead of the ResXFileCodeGenerator, use the PublicResXFileCodeGenerator.

Points of Interest

This is also an example of caching the results, since the data is static and therefore there is no reason to keep enumerating the ResourceSet and transforming it on every request. It's arguable that in this example the cache lookup is just as expensive as recreating the result each time; however, knowing techniques to deal with concurrency and caching is needed in any skillset.

History

  • 9th November, 2014: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --