Introduction
Occasionally the need arises to pass a set of name/value pairs from a client application to a service method (and/or perhaps,
in the other direction). The preferred in-memory repository for name/value pairs is the Dictionary<string, string>
class.
However, the generic dictionary classes are not serializable via standard mechanisms, so a little custom code must be written. There is a "natural" invertible
mapping from Dictionary<string, string>
to a CSV (comma-separated value) string consisting of name-value nodes. Code to serialize and deserialize
a Dictionary<string, string>
instance in such a way that it can be conveniently passed between a client and service is presented in this article.
Background
Readers may be interested in the specific challenge that led to the problem and solution presented here.
An application needs to send email messages to users. Message body templates, containing replaceable fields, are stored in a database.
I wanted to write a single SendEmail
service method that would send messages, merging data sent by the application into
arbitrary field sets. The specific fields for which data would need to be supplied by the application would be dependent on the message
context, more specifically the ID or name of the message to be sent. For example, one email message might require replacement values for
a set 'A' of five fields, with another email message requiring replacement values for a set 'B' of seven fields. Perhaps sets 'A' and 'B'
would have one or more fields in common, but that need not be the case.
C# provides a lot of flexibility in passing arguments to methods, but in this case we need to pass an arbitrary set of name/value
(or key/value) pairs. How can we do that? Creating a dictionary representing the name/value pairs on the client side and passing that
object to the service method would seem to be a reasonable and convenient approach. The only problem with that strategy is that dictionaries aren't serialized automatically.
Using the code
The solution that's presented here uses explicit method calls to serialize and deserialize the name/value dictionary. It's left as an
exercise for the reader to make the processes implicit, if it seems necessary or desirable to do so.
Two methods are required - one for serialization and one for deserialization. I chose to implement them as extension methods on the
Dictionary<string, string>
and string
classes.
internal static string ToCsv(this Dictionary<string, string> valueByKey,
char separator = ',', char quote = '\x00')
{
StringBuilder sb = new StringBuilder();
foreach (string key in valueByKey.Keys)
{
if (sb.Length > 0)
{
sb.Append(separator);
}
if (quote == '\x00')
{
sb.AppendFormat("{0}={1}", key, valueByKey[key]);
}
else
{
sb.AppendFormat("{0}={1}{2}{3}", key, quote, valueByKey[key], quote);
}
}
return sb.ToString();
}
The serialization method (above) maps the dictionary data into a CSV string. Notice that two special provisions exist, the use of which is optional:
- A character other than the default comma may be selected as the separator character. This feature makes it easy to deal with embedded commas in the data.
- A character may be specified with which to quote the value strings, which can serve as a disambiguating function, similar to the first provision,
and can make the resulting CSV string more readable to humans.
I have encountered the need for both of the disambiguating features, on occasion.
internal static Dictionary<string, string> ToValueByKey(this string csv,
char separator = ',', char quote = '\x00')
{
Dictionary<string, string> resultDictionary = new Dictionary<string, string>();
string[] pairs = csv.Split(separator);
foreach (string pair in pairs)
{
string[] nameValue = pair.Split('=');
if (nameValue.Length == 2)
{
resultDictionary.Add(nameValue[0], nameValue[1].TrimmedOf(quote.ToString()));
}
}
return resultDictionary;
}
internal static string TrimmedOf(this string subject, string charsToTrim)
{
return subject.TrimmedOf(charsToTrim, charsToTrim);
}
internal static string TrimmedOf(this string subject, string charsToTrimLeft, string charsToTrimRight)
{
string result = subject;
int index = 0;
while (index < subject.Length && charsToTrimLeft.Contains(subject[index].ToString()))
{
++index;
}
int lastIndex = subject.Length - 1;
while (lastIndex >= index && charsToTrimRight.Contains(subject[lastIndex].ToString()))
{
--lastIndex;
}
if (lastIndex < index)
{
result = string.Empty;
}
else
{
result = result.Substring(index, lastIndex - index + 1);
}
return result;
}
The deserialization method creates a dictionary, identical to the original, from the CSV string. Options exist, corresponding to those
provided by the serialization method, for recognizing a non-default separator character and for removing a specified quote character from the value substrings.
I'm also providing two overloads of a TrimmedOf
method that's used to remove the quotes, if that feature is required. Of course,
TrimmedOf
could be useful in other situations, as well - so feel free to add it to your general string manipulation toolbox.
It's worth noting in passing that TrimmedOf
provides for the possibility that different left and right quote characters
might be used ('[' and ']', for example), although the serialization/deserialization methods do not extend that potential to their
clients. Furthermore, TrimmedOf
is a more general method (like a sharp multi-functional knife that can be used to remove
peeling and seeds as well as stems, to use a vegan analogy) that just happens to be convenient for lopping off quote characters.
Also, you can't use \x00 (or \0) as a quote character - but I don't expect to receive any complaints about that restriction. :)
This is hardly "rocket science". It's just the sort of relatively mundane custom code that hard-working programmers write very frequently to get things done.
Nevertheless, the code is useful (thoroughly tested, and in actual use), and I hope that someone else will use it and benefit from it.
Perhaps having it pre-written will save someone a few minutes of coding.