String Replacement With Named String Placeholders

I am sure that we have all used the String.Format method to help when concatenating strings. This is great for simple string replacement but the "anonymous" nature of the placeholders can make it confusing with larger operations. I have developed a string helper that lets you used named string placeholders that you pass into a string method.

Let us consider a simple example. Here is the simplest usage of the String.Format method.

string firstNameVariable = "Peter";
string lastNameVariable = "Pan";
String.Format("{0} {1}", firstNameVariable, lastNameVariable)

This will return a string where the value of firstNameVariable replaces the {0} placeholder and the value of lastNameVariable replaces the {1} placeholder. This works nicely and gives you control over the resulting string. For example, you could structure the method call like this.

string firstNameVariable = "Peter";
string lastNameVariable = "Pan";
String.Format("FirstName:{0},  LastName:{1}", firstNameVariable, lastNameVariable)

This would result in FirstName: Peter, LastName: Pan.

However, this process becomes more complex when you use more variables and the source string is more complex. Consider something like this.

string firstNameVariable = "Peter";
string lastNameVariable = "Pan - {0}";
String.Format("FirstName:{0},  LastName:{1}", firstNameVariable, String.Format(lastNameVariable, "Forever a boy"))

There is a little bit of Inception going on here. See the lastNameVariable has a placeholder within it. But it has the same placeholder index as the one in firstNameVariable. This is quite a valid use of placeholders, but can get very confusing if there are a number of them.

A perfect use case of this happening is when you use placeholders in resource strings. You may have a number of resource strings that are generic and so you need to use placeholders. For example, you have a resource string that will be used for the subject line of an email. The resource string text would be "This email is for you." It would be more personalised if it said "This email is for you, Peter". But that information is not available at design time. It needs to be pulled from the session or even a database. So, the resource string is set to "This email is for you, {0}". Then you can do a string replacement to populate the firstname from the database at run-time.

Let's make the use case more complicated now. You have a resource string that gets configuration from your web.config and also data from your database. Furthermore, because we use separation of concerns within our project, we have a web layer, business layer and a data layer. This means we cannot populate all the placeholders at the same time. The string must be populated in stages. That is, web.config text is inserted in the web project, then the username and email address is inserted in the business layer. Look at this example.

public bool PassStringToBusinessLayer()
{
	ResourceManager rm = new ResourceManager("items", Assembly.GetExecutingAssembly());
	string welcomeText = rm.GetString("WelcomeText");	//	text = "Welcome, {0}"
	string firstAndLastName = rm.GetString("FirstAndLastName");	//	text = "{0} {1}"

	string emailBody = String.Format(welcomeText, firstAndLastName);
	_business.PopulateString(emailBody);	//	result = "Welcome, Peter Pan"
	return true;
}

public string PopulateString(string emailBody)
{
	User person = _db.GetCurrentUser();
	string firstName = person.FirstName;	//	Peter
	string lastName = person.LastName;		//	Pan
	return String.Format(emailBody, firstName, lastName);
}

You can see how this string replacement becomes quite complex. The placeholders used by String.Format are zero based which means the first argument always corresponds to the {0} placeholder. This becomes difficult when you want to "nest" variables with placeholders like we did above. A much better solution would be to name our placeholders. That way, instead of having to pass variables in a specific sequence, we can just pass them with a name and have the method put them in the correct place. It's really quite simple.

Firstly, if you have an n-tiered application, you will want to put this string helper in a project that is common to all layers. It's very useful and you will want to use it throughout your application. Here is the string extension method.

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace MVCApp.Domain.Utilities
{
    public static class StringExtension
    {
        public static string Format( this string str, params Expression<Func<string,object>>[] args)
        {
            var parameters = args.ToDictionary( e=>string.Format("{{{0}}}",e.Parameters[0].Name), e=>e.Compile()(e.Parameters[0].Name));

            var sb = new StringBuilder(str);
            foreach(var kv in parameters)
            {
                sb.Replace( kv.Key, kv.Value != null ? kv.Value.ToString() : "");
            }

            return sb.ToString();
        }
    }
}

So StringExtension.Format is the method we will run to replace our strings. And this is what the original code would look like using our StringExtension.Format method.

public bool PassStringToBusinessLayer()
{
	ResourceManager rm = new ResourceManager("items", Assembly.GetExecutingAssembly());
	string welcomeText = rm.GetString("WelcomeText");	//	text = "Welcome, {Name}"
	string firstAndLastName = rm.GetString("FirstAndLastName");	//	text = "{firstname} {lastname}"
	
	string emailBody = StringExtension.Format(welcomeText.Format(
		Name => firstAndLastName
		));

	_business.PopulateString(emailBody);		//	result = "Welcome Peter Pan"

}

public string PopulateString(string emailBody)
{
	User person = _db.GetCurrentUser();
	string firstName = person.FirstName;	//	Peter
	string lastName = person.LastName;		//	Pan
	return StringExtension.Format(emailBody.Format(
		firstname => firstName,
		lastname => lastName
	));	
}

You can see that now we can name our placeholders. The resource strings have placeholder names instead of indexes. This makes it much easier to know where specific content should be replaced and avoids confusion with sequencing.

Til next time ...