Click here to Skip to main content
15,880,405 members
Articles / Programming Languages / C#
Article

Tiny Fluent String API simplifies ToString and Console output

Rate me:
Please Sign up or sign in to vote.
4.79/5 (15 votes)
3 Jun 2012CPOL4 min read 22.2K   162   15   12
Simple extension methods that greatly simplify ToString, Logging, and Debug output.

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:

C#
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:

C#
public string FullName
{
 get { return NamePrefix + " " + FirstName + " " + MiddleInitial 
                         + " " + LastName  + " " + NameSuffix;      }
}

or maybe this:

C#
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:

C#
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:

C#
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:

C#
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) //if (left not empty && right not empty)
    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):

C#
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:

C#
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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Lee has worked on user interfaces, graphics, computational geometry, memory management, threading, and assorted applications in C#, Java, C++, and C. He started out programming in Fortran on a 128 Kb PDP/11, which only proves that he's old, not smart. Lee also writes about chronic illness and his love of animals; his auto racing related articles are here.

Comments and Discussions

 
BugIn your Delimit() function... Pin
Super Lloyd28-Jun-14 16:12
Super Lloyd28-Jun-14 16:12 
GeneralRe: In your Delimit() function... Pin
jfriedman7-Feb-15 11:09
jfriedman7-Feb-15 11:09 
GeneralMy vote of 5 Pin
Paulo Zemek15-Aug-13 16:04
mvaPaulo Zemek15-Aug-13 16:04 
GeneralRe: My vote of 5 Pin
Lee Robie18-Aug-13 11:05
Lee Robie18-Aug-13 11:05 
GeneralMy vote of 5 Pin
JPaula6-Jun-12 9:56
JPaula6-Jun-12 9:56 
very good
GeneralMy vote of 5 Pin
popart4-Jun-12 22:33
popart4-Jun-12 22:33 
GeneralRe: My vote of 5 Pin
Lee Robie5-Jun-12 3:16
Lee Robie5-Jun-12 3:16 
GeneralMy vote of 5 Pin
Florian.Witteler4-Jun-12 22:03
Florian.Witteler4-Jun-12 22:03 
GeneralMy vote of 5 Pin
Marc Brooks4-Jun-12 10:05
Marc Brooks4-Jun-12 10:05 
SuggestionMy 5! (plus a suggestion) Pin
Andreas Gieriet3-Jun-12 11:56
professionalAndreas Gieriet3-Jun-12 11:56 
GeneralRe: My 5! (plus a suggestion) Pin
Lee Robie4-Jun-12 2:38
Lee Robie4-Jun-12 2:38 
GeneralRe: My 5! (plus a suggestion) Pin
Andreas Gieriet4-Jun-12 3:04
professionalAndreas Gieriet4-Jun-12 3:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.