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 theint
. - 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 theIComparer
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?