Introduction
It's fairly common to have C# classes containing a large number of properties (Entity models, MVVM, etc.). Sometimes we need methods that compose
two or more properties, such as "FullName" consisting of first and last name. Also, it's usually helpful to provide an implementation of ToString
containing say the ID, name, and other key properties to improve the debugger's output. And sometimes we need to dump all of the properties for logging and other
console-output type situations. This article shows how a small number of extension methods
that can simplify the code needed to write these types of methods.
Typical Code
For this article, I'm going to use the following typical Person
class:
public class Person
{
public int Id { get; set; }
public string NamePrefix { get; set; }
public string FirstName { get; set; }
public string MiddleInitial { get; set; }
public string LastName { get; set; }
public string NameSuffix { get; set; }
public string Address { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
With a class like this, at some point you end up writing:
public string FullName
{
get { return NamePrefix + " " + FirstName + " " + MiddleInitial
+ " " + LastName + " " + NameSuffix; }
}
or maybe this:
public string FullName
{
get { return string.Format("{0} {1} {2} {3} {4}",
NamePrefix, FirstName, MiddleInitial, LastName, NameSuffix); }
}
In either case, it turns out that C# doesn't mind if some of the properties are null. The problem is the delimiting spaces between the parts of the name: this code includes
all of the delimiters, whether needed or not. If some of the properties are null, the formatting will be wrong.
For my ToString
method, I like to include the entity ID along with full name, like this:
public override string ToString() { return "Id[" + Id + "] " + FullName; }
but I want to only display the ID if it's non-zero, which this code doesn't do.
Solution: Composable Extension Methods
My solution is a small set of extension methods - seven for strings and one for ints. The workhorse method is Delimit
,
which glues two strings together, and includes the delimiter only when both strings are non-empty. Using Delimit
, our FullName
property looks like this:
public string FullName
{ get { return NamePrefix.Delimit(FirstName).Delimit(MiddleInitial)
.Delimit(LastName).Delimit(NameSuffix); }
}
This implementation puts in delimiters only when necessary, and more importantly, returns the empty string if all parts of the name are empty.
The implementation of Delimit
is:
public static string Delimit(this string left, string right, string delimiter = " ")
{
int n = 0;
if (string.IsNullOrWhiteSpace(left)) { n++; left = string.Empty; }
if (string.IsNullOrWhiteSpace(right)) { n++; right = string.Empty; }
if (n == 0)
left += delimiter;
left += right;
return left;
}
As you see, if either string is null or empty, it returns the other. If both are non-empty, it separates them by a delimiter: a single space by default, or some other string you pass in.
The corrected ToString
method looks like this (the reason to separate this into two methods will be clear in a moment):
private string IdString { get { return Id.Prefix("Id[").Suffix("]"); } }
public override string ToString() { return IdString.Delimit(FullName, "-"); }
These methods use the same sort of logic: add a prefix or suffix if the base string is non-empty, or in this case if the ID is non-zero.
ToString
shows the use of an optional separator, and produces output like this:
Person: // Person with all default / null values
Person: John Smith // Person with first and last name only
Person: Id[15]-Ms. Jane Q Doe Phd // Person with non-zero Id and all properties defined
Here is the implementation of Prefix
and Suffix
:
public static string Prefix(this int id, string prefix)
{
string ids = id != 0 ? id.ToString() : string.Empty;
return ids.Prefix(prefix);
}
public static string Prefix(this string left, string prefix)
{ return string.IsNullOrWhiteSpace(left) ? string.Empty : prefix + left; }
public static string Suffix(this string left, string suffix)
{ return string.IsNullOrWhiteSpace(left) ? string.Empty : left + suffix; }
Multi-Line Output
In certain cases, like logging, I want to output a complete dump of an object, like this:
--Person--
Database: Id[15]
Fullname: Ms. Jane Q Doe Phd
Address1: 555 Fifth St.
Address2: Apartment 1
CityStZp: Cincinnati, OH 45203
That is, each non-empty property (or collection of properties) is on its own line, indented with a label. I use the name ToDetailedString
for this;
the implementation for Person
is:
public string ToDetailedString()
{
return IdString.Prefix("Database: ").Indent()
.NLIndent(FullName.Prefix("Fullname: "))
.NLIndent(Address.Prefix("Address1: "))
.NLIndent(Address2.Indent().Prefix("Address2: "))
.NLIndent(City.Delimit(State, ", ").Delimit(ZipCode).Prefix("CityStZp: "))
.Prefix("--Person--".NL() );
}
Here you see the use of the remaining methods. Indent
indents a non-empty string some number of spaces, and NLIndent
does the same
with a newline thrown in. In the case of an uninitialized object, ToDetailedString
returns nothing (the empty string) as expected.
Also, I'm reusing the IdString
property here.
In all of this, you see a kind of reverse-polish notation where Prefix
comes after its argument. For examle, the "--Person--" heading has to be coded
at the end of the expression. The conditional logic in the extension methods require the complete string, so it has to be this way.
I should point out that this type of string manipulation does add some abuse of the garbage collector. The recommended way to minimize garbage is to use
a StringBuilder
, but this didn't work for me. I created a class (CP) that wrapped the builder, and implemented methods like Delimit
and Prefix
. You then need a way to create the CP object; the manipulators all return it, and you also need something to trigger StringBuilder.ToString()
at the end.
This turns out to significantly complicate the syntax; in some cases you need more than one of these CP objects for the logic to fall out correctly.
So I stuck with the simple approach presented here on the assumption that this is primarily used for debugging and logging.
Summing Up
That's it: a tiny set of extension methods (~140 lines commented) in a single file. The attached project contains the examples used here along with the Compost extension methods.
Recently I added ToDetailedString
methods to an Entity Framework project that had both composition and inheritance,
and got quick, valuable data dumps. I hope this approach will be of use in your projects as well.
History
- June 1, 2012 - Original article.