2

Normally, when we have a template file and some text substitutions to run, we create a Dictionary<string,string> that looks something like:

Dictionary<string,string> substitutions = new Dictionary<string, string>
{
    {"{firstname}", customer.FirstName },
    {"{lastname}", customer.LastName },
    {"{address}", "your address" },
    {"{siteurl}", _siteUrl },
};

if (customer.Address != null)
{
    substitutions["{address}"] = customer.GetAddress();
}

and so on. Then we can do something like:

email.Body = substitutions.Aggregate(template,
    (current, sub) => current.Replace(sub.Key, sub.Value));

to get the substituted template.

I've had a situation today where I've needed to ensure that the substitutions ran in a particular order.

Now I could just ensure that they're put into the Dictionary in the right order and hope that the arbitrary order they get enumerated in maintains that sequence — I've never seen a Dictionary be enumerated in some other sequence, but the order of an ICollection is not guaranteed.

So it struck me it'd be useful to do something like this (where i++ is being used as a placeholder for "any value":

SomeCollection<string,string,int> substitutions
    = new SomeCollection<string, string, int>
{
    {"{firstname}", customer.FirstName, i++ },
    {"{lastname}", customer.LastName, i++ },
    {"{address}", "your address", i++ },
    {"{siteurl}", _siteUrl, Int32.MaxValue }, // this should happen last
};

if (customer.Address != null)
{
    substitutions["{address}"] = customer.GetAddress();
}

and I could push through an IComparer of some kind to sort on the int value.

But then I tried to work out how to make such a collection and, after an abortive attempt at writing something backed by a Dictionary<string, Tuple<int, string>>, decided that the elegance of my code was not worth the amount of stress it was causing me (given the deadline and so on) and I could just add the .Replace("{siteurl}", _siteUrl) to the end of my Aggregate call and it'd do the job adequately.

But it's bugging me that I've given up on what could have been a nice elegant thing. The problem I was coming across (apart from trying to wrangle a Dictionary<string, Tuple<int, string>> to be an implementation of ICollection<KeyValuePair<string,string>> and work out how to implement the GetEnumerator methods whilst trying not to stress out about a deadline) was that I want the following:

  • The ability to declare it simply using the object initialiser syntax above.
  • The ability to get and set members by key (hence backing with a Dictionary<string, Tuple<int, string>>).
  • The ability to have a foreach loop pull things out in order of the int.
  • The ability to add (or initialise) items without specifying the int if I don't care about that item's sort position.
  • The ability to perform the substitutions using something relatively terse, like the Aggregate call above (probably passing in the IComparer I didn't get as far as writing).

What I was getting stuck on was the GetEnumerator implementations and my failure to remember that indexer overloading isn't difficult.

How would you implement such a requirement. Was I on the right track or did I overlook something more elegant? Should I have just stuck with a Dictionary<string,string> and come up with some way to insert new items at the start or end — or just middle if I don't care for that item?

What beautiful, elegant solution did I just not quite get to? How would you have met that need?

Owen Blacker
  • 4,117
  • 2
  • 33
  • 70
  • Other than putting the sort order into the object initialisation literals, which is just syntatic sugar, why can't you use an OrderedDictionary? Then do the replacements with something like `myOrderedDictionary.OfType().Aggregate(template, (x,y) => x.Replace(y.Key.ToString(), y.Value.ToString()))` but in that case you may as well use Jim Mischel solution but be more elegant/terse/complex with `subs.OrderBy(x => x.Value.Item2).Aggregate(template, (x,y) => x.Replace(y.Key, y.Value.Item1))`. Writing a specific collection type to do this doesn't seem worth the effort? – robwilliams Feb 08 '13 at 09:36
  • @robwilliams I don'think I can use an `OrderedDictionary`, as that would sort by the key, not by my arbitrary other sortorder. – Owen Blacker Feb 08 '13 at 16:05

2 Answers2

1

Seems like you could just use LINQ to sort by the substitution order and then enumerate. For example, if you have your Dictionary<string, Tuple<string, int>>, it would look something like:

Dictionary<string, Tuple<string, int>> subs = new Dictionary<string, Tuple<string, int>>
{
    {"{firstname}", Tuple.Create(customer.FirstName, i++) },
    {"{lastname}", Tuple.Create(customer.LastName, i++) },
};

// Now order by substitution order
var ordered =
   from kvp in subs
   orderby kvp.Value.Item2
   select kvp;
foreach (var kvp in ordered)
{
    // apply substitution
}

By the way, Dictionary<TKey, TValue> does not guarantee that an enumeration will return the items in the order in which they were added. I seem to remember getting burned by counting on the order at some point, but it might have been some other collection class. In any case, counting on undocumented behavior is just asking for something to go wrong.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • Yeah, I was aware that `Dictionary` is not guaranteed to come out in the "right" order, hence my comment "the order of an `ICollection` is not guaranteed" after the first set of code blocks. That's why I'm worried about using another sortorder. It looks like your solution would achieve that, though, with Rob Williams's suggested amendment to the `Aggregate` call. The use of `Tuple.Create` is definitely nicer than I'd managed, but still slightly less elegant-looking than I'd like. Nice answer, though; thank you! – Owen Blacker Feb 08 '13 at 16:10
0

Why are entries in addition order in a .Net Dictionary?

This question may help you out a bit - iterating over a dictionary's keys in current versions of .NET will return keys in order, but since this isn't guaranteed by the contract, it could change in future versions of .NET. If this worries you, read on. If not, just iterate over the Dictionary.

The easiest solution, in my opinion, would be to stick with your Dictionary<string, string> approach and maintain a separate List<string> substitutionOrder. Iterate over this list, and use the values to index into your dictionary.

email.Body = substitutionOrder.Aggregate(template,
    (current, sub) => current.Replace(substitutions[sub].Key, substitutions[sub].Value));
Community
  • 1
  • 1
Jeff-Meadows
  • 2,154
  • 18
  • 25
  • I'm not sure I understand how having two `ICollection`s that might (and only might) come out in the "right" order would help me. I might be misunderstanding your suggestion, though. – Owen Blacker Feb 08 '13 at 16:11
  • I suggested using a `List`, which *will* come out in the "right" order (http://stackoverflow.com/questions/612486/is-the-net-foreach-statement-guaranteed-to-iterate-a-collection-in-the-same-ord). If you prefer, you could use a `string[]` instead, which will allow for initializer syntax. I think you'll believe that an array will iterate in the "right" order. – Jeff-Meadows Feb 08 '13 at 17:54
  • Ah, I didn't realise that a `List` was _guaranteed_ to come out in insertion order (like a `Queue`, effectively). I thought the order wasn't guaranteed but, if [Jon Skeet](http://stackoverflow.com/users/22656/jon-skeet) says it's true, it must be ;o) – Owen Blacker Feb 11 '13 at 09:54