Skip to content

Instantly share code, notes, and snippets.

@BhaaLseN
Last active December 24, 2017 13:18
Show Gist options
  • Save BhaaLseN/64ba9b336789a050fcc1398ed8f6ee49 to your computer and use it in GitHub Desktop.
Save BhaaLseN/64ba9b336789a050fcc1398ed8f6ee49 to your computer and use it in GitHub Desktop.
Resource message formatting

L10n of messages

One of the most common ways of localizing messages for different languages is using .NET Resource files (.resx). Access is always done through the ResourceManager class, regardless of whether accessor properties are generated or not; but the actual language selection is done internally by switching the current threads' Culture (implicitly, or by passing a Culture explicitly)

A good way of dealing with those is providing accessor methods that take strongly typed parameters (to make it easier for collaborators to do the right thing) and simply retrieve/format the matching resource.

The Classic approach (Messages_Classic.cs)

ResourceManager.GetString returns a named resource and can be passed into string.Format if necessary. The Accessor method is strongly typed and has (more or less) meaningful parameter names to guide collaborators, such as string CountryHasAnAreaOf(string countryName, long areaSizeInSquareKilometers) (which may return something like "The Country '{0}' has an area of {1}m²".)

The message code is passed as a string, which is prone to typos. Starting with C#6, nameof(CurrentMethod) can be used to avoid this (since the message code and the actual resource key are more than likely to match so they can be matched much more quickly.) But even with nameof, chances are that a developer copy&pastes an existing message and forgets to update the message code parameter.

The Delegate approach (Messages_Delegate.cs)

To work around this potential copy&paste issue (as well as alleviating the DRY problem of putting the same name/magic string in multiple places) we'd like to use CallerMemberNameAttribute instead, which at compile time hardcodes the current callers name. This requires the use of default parameters, which don't play very nice with params (since it would be hard or impossible to distinguish where the params array ends and an optional parameter begins; or whether a given positional argument should go in the params array or the optional parameter.)

We needs some creative thinking, and put the params part elsewhere. This approach uses a Delegate as return value which then takes the params portion of the message. The first call does nothing but capture the caller and wrap it into a delegate that takes a params array. This delegate is returned and needs to be called to generate the actual message by augmenting it with format parameters.

The syntax looks weird, but given the indirection through a delegate it only takes a very miniscule performance hit compared to the Classic approach. Even assuming this is called a few thousand times in a tight loop, the runtime difference is barely hundredths of a fraction of a second across all calls.

The Lambda approach (Messages_Lambda.cs)

To get rid of the double-braces when retrieving a message, we can try something different: a Delegate parameter that takes a lambda. Instead of returning a delegate, we let the caller pass in a delegate as parameter to a delegate (yes, nested!) The implementation provides the inner portion of the delegate which is the same as with the Delegate approach: capture the message code and pass it to a formatting function that also takes the format parameters. The outer portion of the delegate is provided by the caller; and does nothing else than call our inner portion with the needed format parameters. Dubbed the "Lambda" approach because the outer portion makes sense as simple lambda expression.

Looks slightly nicer than the Delegate approach, but turns out to be the slowest of the three due to double indirection. But again, we're barely talking hundredths of a fraction of a second here.

Performance Comparison

Testing was done with 10k runs in a loop after a warm-up period that isn't included in the total. Every iteration calls 4 message accessor methods in a row with the total being recorded for later.

Approach Min Avg Max
Classic 10.4553ms 10.6717ms 11.0655ms
Delegate 11.5051ms 11.8092ms 12.1331ms
Lambda 11.6894ms 11.90252ms 12.2617ms

Times are total for all 10k iterations with 4 calls each (so 40k calls in total.) Individual results would be somewhere around 1µs for all 4 calls, or around 200-300ns for each individual call (depending on whether we need formatting or not.)

Basically, timing differences are negligible unless you got some really specific requirements with very tight timings.

A Note on Guard

The Guard structure has no practical use other than triggering overload resolution. Assuming there is a formal string parameter that should be passed as message code, another formal string parameter describing a format parameter for the message and a third formal string parameter that is a default parameter for CallerMemberName, passing too many strings there might overwrite the default parameter meant to receive the caller name. To avoid this, a sentinel/guard parameter is introduced which is never meant to be written out in source (as the parameter name states.) It would cause a compilation error if a string was passed in the place where this Guard struct is placed (which either means someone is using it wrong, or we don't have enough overloads; depending on where the guard is.)

<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NoParam" xml:space="preserve">
<value>No Parameters here</value>
</data>
<data name="OneIntParam" xml:space="preserve">
<value>Just a Number: {0}</value>
</data>
<data name="OneStringParam" xml:space="preserve">
<value>This is the {0} string</value>
</data>
<data name="ThreeParams" xml:space="preserve">
<value>It is said that {0} and {1} is {2}</value>
</data>
</root>
using System.Linq;
using System.Resources;
namespace ConsoleApp1
{
// Helper base-class to make testing slightly easier
public abstract class Messages_Base
{
private static ResourceManager ResourceManager { get; } = new ResourceManager("ConsoleApp1.Messages", typeof(Messages_Base).Assembly);
// this method actually returns the requested message (or the message code itself when not found)
// and optionally calls string.Format to replace format parameters.
protected string GetMessageInternal(string messageCode, params object[] args)
{
string resourceMessage = ResourceManager.GetString(messageCode);
if (args.Any())
resourceMessage = string.Format(resourceMessage, args);
return resourceMessage;
}
public abstract string NoParam();
public abstract string OneIntParam(int number);
public abstract string OneStringParam(string str);
public abstract string ThreeParams(string one, string two, string three);
}
}
namespace ConsoleApp1
{
// Classic messages class that simply retrieves the message by name and then formats it with additional parameters.
// Prone to errors because the resource name must be specified (C#6 nameof helps, but is still prone to copy&paste errors).
// Fastest way to do it (and portable until the very first C# version), but barely noticable in practise compared to the other alternatives.
public class Messages_Classic : Messages_Base
{
public override string NoParam() => GetMessageInternal(nameof(NoParam));
public override string OneIntParam(int number) => GetMessageInternal(nameof(OneIntParam), number);
public override string OneStringParam(string str) => GetMessageInternal(nameof(OneStringParam), str);
public override string ThreeParams(string one, string two, string three) => GetMessageInternal(nameof(ThreeParams), one, two, three);
}
}
using System.Runtime.CompilerServices;
namespace ConsoleApp1
{
// More modern approach that relies on CallerMemberNameAttribute (C#5) to retrieve the message name safely without copy&paste.
// Syntax looks a little weird because the first method returns a delegate that needs to be called;
// and only this second call takes parameters as necessary. This cannot be done otherwise because params does not work with optional parameters.
// A tiny bit slower than the classic approach (about one and a half times slower), but again barely noticable.
public class Messages_Delegate : Messages_Base
{
public override string NoParam() => GetMessage()();
public override string OneIntParam(int number) => GetMessage()(number);
public override string OneStringParam(string str) => GetMessage()(str);
public override string ThreeParams(string one, string two, string three) => GetMessage()(one, two, three);
private delegate string ArgsFunc(params object[] args);
private ArgsFunc GetMessage(Guard neverPassThisParameter = default, [CallerMemberName] string messageCode = null)
=> args => GetMessageInternal(messageCode, args);
// Guard struct to make sure nobody actually passes optional parameters;
// or overflows into the inferred messageCode by accident.
private struct Guard { }
}
}
using System;
using System.Runtime.CompilerServices;
namespace ConsoleApp1
{
// Another modern approach that uses CallerMemberNameAttribute (C#5), but instead of delegates it uses a lambda parameter
// which then passes in the necessary format arguments. This moves the params part elsewhere and allows the use of default parameters.
// The slowest of the three (about twice as slow as Classic), but yet barely noticable since we're talking millionths of a fraction of a second here.
public class Messages_Lambda : Messages_Base
{
public override string NoParam() => GetMessage();
public override string OneIntParam(int number) => GetMessage(m => m(number));
public override string OneStringParam(string str) => GetMessage(m => m(str));
public override string ThreeParams(string one, string two, string three) => GetMessage(m => m(one, two, three));
private string GetMessage(Guard neverPassThisParameter = default, [CallerMemberName] string messageCode = null)
=> GetMessageInternal(messageCode);
private delegate string FormatFunc(params object[] args);
private string GetMessage(Func<FormatFunc, string> format, Guard neverPassThisParameter = default, [CallerMemberName] string messageCode = null)
=> format(args => GetMessageInternal(messageCode, args));
// Guard struct to make sure nobody actually passes optional parameters;
// or overflows into the inferred messageCode by accident.
private struct Guard { }
}
}
using System;
namespace ConsoleApp1
{
class Program
{
static void Main()
{
Test<Messages_Classic>();
Test<Messages_Lambda>();
Test<Messages_Delegate>();
}
static void Test<T>() where T : Messages_Base, new()
{
var messages = new T();
Console.WriteLine(messages.NoParam());
Console.WriteLine(messages.OneIntParam(42));
Console.WriteLine(messages.OneStringParam("awesome"));
Console.WriteLine(messages.ThreeParams("one", "two", "three"));
Console.WriteLine();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment