Macro Resolution via Dictionaries

In my Systems Directorate application, I needed a way of handling macro resolution so that the user could easily substitute values when configuring parts of the application.

For most of the application, I have standardized on a Dictionary<string,object> structure for storing information. I retrieve records from the database this way, I store message events in structures like it, etc. I have systems that convert the dictionaries to tab delimited tables when I store them in files or read them back in. Even type information is retained.

The macro format I wish to support is the same way it is done in UNIX shell scripts and other script languages. Eg. ${NAME}.

To add the functionality to the dictionary, I created a set of extension methods. The main method also takes a second dictionary in case there is another source of data it can pull values from. It uses that second dictionary first, then the source dictionary. This allows for a main list of macro information, and then overridden values for an item instance. Other method signatures are available that hide this extra functionality if it is not needed.

To make it even more useful, failing to find a value in the instanceData or the main dictionary, it will try searching for a value in the system environment. If all have failed, it will replace the macro with an empty string if the replaceUnknownWithEmptyString parameter is true. If false, it leaves the macro untouched.

/// <summary>
/// Given an input string and this set of variables, it replaces all variable occurrences in the string with their
/// values.
/// </summary>
/// <param name="data">The dictionary to pull variables from.</param>
/// <param name="inputText">The input text to convert.</param>
/// <param name="instanceData">Optional data that is being evaluated that could provide another source of resolution</param>
/// <param name="replaceUnknownWithEmptyString">If a variable is not found, replace the value with an empty string if this is set.</param>
/// <returns>A resolved string.</returns>
public static string Resolve(
	this Dictionary<string, object> data, 
	string inputText, 
	Dictionary<string, object> instanceData, 
	bool replaceUnknownWithEmptyString) 
{
…
}

How it works

In order to support macros within macros, it runs through a simple loop and searches for the ${ opening brace of the macro. It then finds the matching end brace and collects the variable name from in between.

Resolving all values in a dictionary

Another extension I added was the ability to look through all the values in a dictionary and resolve any macros by using other values in the dictionary itself. When finished, the only macros left in any of the dictionary values are ones that do not exist as keys.

Example

Here is a quick example of it being used.

Dictionary d = new Dictionary<string,object>();
d["VAL1"] = "Hello";
d["VAL2"] = "World";
string s = "${VAL1} ${VAL2}";
string result = d.Resolve(s);

// Outputs "Hello World"

String extension

To make the usage a bit more natural for strings and similar to a Python syntax, I also added a Resolve method to the string class with another extension. It simply reverses the call and calls the dictionary Resolve method.

public static string Resolve(this string input, Dictionary<string, object> data) {
	return data.Resolve(input);
}

Code

/// <summary>
/// Gets a value from the dictionary in string form.  Applies desired formatting to the string results.
/// </summary>
/// <param name="data">The object to get the text from.</param>
/// <returns>The text value.</returns>
public static string GetValue(object data) {
	if (data != null) {
		switch (data.GetType().Name) {
			case "DateTime":
				return ((DateTime)data).ToString("yyyy/MM/dd HH:mm:ss");
			default:
				return data.ToString();
		}
	}
	return "NULL";
}

/// <summary>
/// Given an input string and this set of variables, it replaces all variable occurrences in the string with their
/// values.  Unfound variables will not be replaced.
/// </summary>
/// <param name="data">The dictionary to pull variables from.</param>
/// <param name="inputText">The input text to convert.</param>
/// <returns>A resolved string.</returns>
public static string Resolve(this Dictionary<string, object> data, string inputText) {
	return data.Resolve(inputText, null, false);
}

/// <summary>
/// Given an input string and this set of variables, it replaces all variable occurrences in the string with their
/// values. 
/// </summary>
/// <param name="data">The dictionary to pull variables from.</param>
/// <param name="inputText">The input text to convert.</param>
/// <param name="replaceUnknownWithEmptyString">If a variable is not found, replace the value with an empty string if this is set.</param>
/// <returns>A resolved string.</returns>
public static string Resolve(this Dictionary<string, object> data, string inputText, bool replaceUnknownWithEmptyString) {
	return data.Resolve(inputText, null, replaceUnknownWithEmptyString);
}

/// <summary>
/// Given an input string and this set of variables, it replaces all variable occurrences in the string with their
/// values.  Unfound variables will not be replaced.
/// </summary>
/// <param name="data">The dictionary to pull variables from.</param>
/// <param name="inputText">The input text to convert.</param>
/// <param name="instanceData">Optional data that is being evaluated that could provide another source of resolution</param>
/// <returns>A resolved string.</returns>
public static string Resolve(this Dictionary<string, object> data, string inputText, Dictionary<string, object> instanceData) {
	return data.Resolve(inputText, instanceData, false);
}

/// <summary>
/// Given an input string and this set of variables, it replaces all variable occurrences in the string with their
/// values.
/// </summary>
/// <param name="data">The dictionary to pull variables from.</param>
/// <param name="inputText">The input text to convert.</param>
/// <param name="instanceData">Optional data that is being evaluated that could provide another source of resolution</param>
/// <param name="replaceUnknownWithEmptyString">If a variable is not found, replace the value with an empty string if this is set.</param>
/// <returns>A resolved string.</returns>
public static string Resolve(this Dictionary<string, object> data, string inputText, Dictionary<string, object> instanceData, bool replaceUnknownWithEmptyString) {
	int startPosition = 0;
	if (inputText == null) {
		inputText = "";
	}

	do {
		//
		// Find the start token, if not found abort
		//
		int startIndex = inputText.IndexOf("${", startPosition);
		if (startIndex == -1) {
			break;
		}
		startPosition = startIndex + 1;

		//
		// Find the end token, if not found abort
		//
		int endIndex = inputText.IndexOf("}", startIndex);
		if (endIndex == -1) {
			break;
		}
		//startPosition = endIndex + 1;

		//
		// Extract the variable tag ${ABC}, then ABC
		//
		string variableTag = inputText.Substring(startIndex, (endIndex - startIndex) + 1);
		string variableName = variableTag.Substring(2, variableTag.Length - 3);
		string variableValue = "";

		//
		// Do the replacement.  Search in instance data, current data, environment.
		//
		if (instanceData != null && instanceData.ContainsKey(variableName)) {
			variableValue = GetValue(instanceData[variableName]);
			inputText = inputText.Replace(variableTag, variableValue);
		} else if (data.ContainsKey(variableName)) {
			variableValue = GetValue(data[variableName]);
			inputText = inputText.Replace(variableTag, variableValue);
		} else {
			string value = Environment.GetEnvironmentVariable(variableName);
			if (!string.IsNullOrEmpty(value)) {
				variableValue = value;
				inputText = inputText.Replace(variableTag, variableValue);
			} else {
				if (replaceUnknownWithEmptyString) {
					inputText = inputText.Replace(variableTag, "");
				}
			}
		}
	} while (true && startPosition < inputText.Length);

	return inputText;
}

/// <summary>
/// All columns in the dictionary are looked at and any unresolved variables found will be resolved using 
/// data from the dictionary itself.  Variables have the form ${name} where name will be another key in the
/// dictionary.
/// </summary>
/// <param name="data">The dictionary to process.</param>
public static void ResolveAll(this Dictionary<string, object> data) {
	bool altered;
	int count = 0;

	do {
		altered = false;
		List<string> keys = data.Keys.ToList();

		foreach (var key in keys) {
			if (data[key] is string) {
				string value = data[key] as string;

				if (value.IndexOf("${") >= 0) {
					string result = data.Resolve(value, null, false);
					data[key] = result;
					altered = true;
				}
			}
		}
	} while (altered && count++ < 50);
}

public static string Resolve(this string input, Dictionary<string, object> data) {
	return data.Resolve(input);
}
This entry was posted in Dennis-IT Tools and tagged , , , . Bookmark the permalink.