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 too 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.
Thread.CurrentThread.CurrentUICulture = cultureInfo;
Thread.CurrentThread.CurrentCulture = cultureInfo;
Client
Clients of this service want an API to get the following:
- The
ResourceSet
that is requested, and
- 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 string
s.
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:
{
"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
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
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) };
[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);
var result = value != null ? value.Value : newValue;
return Ok(result);
}
}
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