Expansive
- having a capacity or tendency to expand.
- causing or tending to cause expansion
- characterized by richness, abundance or magnificence
While working on a recent project involving lots of appSettings keys and values in my web.config/app.config files and some values that have overlapping information (server names, file paths, etc.), I realized how much I missed the property expansion capabilities of NAnt and, more recently, MSBuild. So, I set out to see if I could quickly build a simple string property expansion library to solve this common problem and maybe some others along the way.
While I knew that true string interpolation would be somewhat tricky and probably best left to the framework developers, I felt that an easy add-on API using extension methods and possibly some new helper classes could at least make my life easier.
Channeling Rob Conery, I named my new library Expansive as a play on Expansion and Expensive (sarcasm) as I wanted something that wasn’t expensive (technically) and was easy to implement and easy to learn.
You can check out the full capabilities via the readme over on GitHub, but I’ll give you a couple of examples and ideas on how it can be used.
1. Solve web.config / app.config nightmares
Many times I find that in large applications, I have many web.config / app.config appSettings keys, connection strings, etc and that many of the values have redundant information. For instance server names, IP Addresses or paths may be strewn throughout several appSettings keys and I find myself wishing I could just define the data once, so as to “normalize” my appSettings, and then simply reference one key from the value of another. What I really want is a composition model where I can compose different appSettings (or other strings for that matter) using previously defined values.
Here’s a common scenario:
- I may have a connection string defined with a server name: dbserver01
- I may have environment specific naming conventions like (dbserver01-dev, dbserver01-qa, dbserver01-stg, dbserver01-prod, etc)
- I may have other server names in my appSettings like urls for services, unc paths for fileshares, report server urls, etc.
- While I can certainly duplicate the server names and environment suffix throughout the various appSettings keys, it quickly becomes a chore to make sure these are all correct for a specific environment.
- Web.config transforms help a little bit in that they allow me to define values for different configurations, but it still doesn’t give me a composition model where I can simplify my web.config / app.config settings so that they are easier to manage and maintain. Plus web.config transforms only help at build/deploy time. There is no story for runtime changes.
Expansive aims to solve that problem by allowing for NAnt-like property expansion (scroll down to section 6) whereby I can define an appSetting once such as:
<add key="env" value="dev"/>
And then be able to reuse that value in other settings such as:
<add key="ServerName" value="dbserver01-{env}"/>
And, then finally use that value inside of a connection string such as:
<add name="Default" connectionString="server={ServerName};Initial Catalog=master;uid=uid;pwd=pwd" provider="System.Data.SqlClient"/>
As you can see, I’m defining the “env“ setting once and then reusing that in the “ServerName” setting, which is then used in the connection string.
To get the “expanded” values, you simply call the Expand() extension method on string. For instance:
var connectionString = ConfigurationManager.ConnectionStrings["Default"].ConnectionString.Expand(); // returns "server=dbserver01-dev;Initial Catalog=master;uid=uid;pwd=pwd"
or you can use the dynamic ConfigurationManager wrapper “Config” as follows:
var connectionString = Config.ConnectionStrings.Default; // returns "server=dbserver01-dev;Initial Catalog=master;uid=uid;pwd=pwd" without needing explicit call to Expand()
By default expansive looks in your web.config or app.config appSettings for the tokens to lookup and expand. However, you can change this or provide your own “source” in the form of a Func<string, string> lambda on the call to Expand() which you’ll see in one of the examples below.
While you obviously have to take care in setting this up the first time, it allows for easily making changes to a series of settings by following a composition model where settings build on one another.
This web.config / app.config example is probably the most common, but this can also be used for string formatting / templating.
2. String Formatting
How many times have you written something like:
var welcomeMessage = string.Format("Hello, {0}", "John");
While it’s a fairly simple example and pretty easy to see from the argument context that the name “John” will get substituted in place of the “{0},” often times these types of strings are in a resource file, config file or database and when looking at them alone and without argument context, it’s not always easy to see what the ‘expectations’ of that string are. For instance if this were localized, it might just read “{0}, {1}” in a resource file. While the key may be something like “welcomeMessage,” it still may not be immediately apparent what that string format requires.
How much more readable and explicit would it be if we could write:
// Simple positional based formatting // Essentially gets transformed into string.Format("{0} {1}","Hello", "John") var welcomeMessage = "{greeting}, {firstName}".Expand("Hello", "John");
This is is the simple alternative to string.Format() that Expansive provides in the form of an a simple Expand() extension method on string.
Remember, if I hadn’t provided the arguments (“Hello” and “John”) to the Expand() method, it would have looked for “greeting” and “firstName” keys in my web.config / app.config appSettings. Again, this is just the default, but you can change it globally or on a per call basis. For instance, you could tell Expansive to pull these values out of resource file, a database table, from a webservice, what have you.
Some other ways in which you could accomplish the same thing with Expansive would be:
// Same as above just with variables defined first // NOTE: This is positional replacement, not actual string interpolation based on variable names // Again, this gets tranformed into string.Format("{0}, {1}", "Hello", "John") var greeting = "Hello"; var firstName = "John"; var welcomeMessage = "{greeting}, {firstName}".Expand(greeting, firstName);
or
// Example using a Dictionary<string, string> and containing key/value pairs for expansion // This could come from a database, file, web service, etc. var resourceDictionary = new Dictionary<string, string> { {"greeting","Hello"} ,{"firstName","John"} }; // call Expand() passing in a custom Func<string, string> lamda for key/value lookup var welcomeMessage = "{greeting}, {firstName}".Expand(key => resourceDictionary[key]);
Note, you could also do this:
// Example of reusing tokens inside of another string // NOTE: This is positional replacement, not actual string interpolation based on variable names // This essentially becomes string.Format("FirstName: {0}, LastName:{1}, FullName: {0} {1}", "John", "Smith") var firstName = "John"; var lastName = "Smith"; var fullName = "{firstName} {lastName}" var welcomeMessage = "FirstName: {firstName}, LastName: {lastName}, FullName: {fullName}".Expand(firstName, lastName, fullName); // NOTE: For fullName to work above, the "{firstName}" "{lastName}" tokens must have been in the string to be expanded.
So, as you can see this is pretty powerful and easy to read in that you can assign friendly names to the tokens vs. just have {0}, {1}, {2}… etc.
3. Model-based Templating
Additionally, Expansive can be used for model-based templating using the Expand(object model) method.
This allows you to do the following:
var model = new { FirstName = "John", LastName = "Smith" }; var testString = "First:{FirstName}, Last:{LastName}".Expand(model); // returns "First:John, Last:Smith";
Here, Expansive replaces the tokens in the string with the values of the corresponding properties on the passed model object. Nice and clean!
Arguably, this is the most powerful feature and my favorite to use, but I’m curious to get your take.
Download it from GitHub or install it using NuGet and let me know what you think.
Enjoy!