DateTime
has a great feature that I often replicate in my classes: the ability for users to format the ToString
output however they want. Using it is cool, but building it yourself? Even better.
In this post, I’ll walk you through implementing custom formatting for any class using the IFormattable interface, covering multiple approaches.
You can also check out my YouTube video on the same topic here: https://youtu.be/hocPk1eBUuE
What we’re aiming to achieve is similar to the ToString
method with format options found in the DateTime
class.
Console.WriteLine(DateTime.Now.ToString("(MM) d - yyyy"));
// Output: (11) 18 - 2024
You’ll see that we can pass a format string to the ToString
method, and based on what we specify, we get the corresponding data. Plus, you can customize the output by adding multiple “tokens” to the format string.
So, let’s get started by creating a new class called Employee
.
public class Employee
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string JobTitle { get; set; }
public required string CompanyName { get; set; }
}
Next, we’ll implement the IFormattable
interface and add a new ToString
method.
Stay In Touch
Subscribe to our mailing list to stay updated on topics and videos related to .NET, Azure, and DevOps!
By submitting your information, you’re giving us permission to email you. You may unsubscribe at any time.
public class Employee : IFormattable
{
// (..)
public string ToString(string? format, IFormatProvider? formatProvider)
{
throw new NotImplementedException();
}
}
I’ll override the ToString
method and create another overload with a single parameter for the format.
public class Employee : IFormattable
{
// (..)
public override string ToString()
{
return ToString("G"); // G as General format
}
public string ToString(string? format)
{
return ToString(format, CultureInfo.CurrentCulture); // use CurrentCulture for now
}
public string ToString(string? format, IFormatProvider? formatProvider, int x)
{
// Let's start
}
}
Simple Approach
The first approach is straightforward: mapping a “format token” to an actual output. For instance, using the format Login
should display FirstName.LastName
.
public string ToString(string? format, IFormatProvider? formatProvider)
{
// G, Full => F L is the JobTitle at CompanyName
// login, L => firstname.lastname
// userdomain, ud => [email protected]
if (string.IsNullOrEmpty(format))
{
format = "G";
}
format = format.ToUpper();
return format switch
{
"G" or "FULL" => $"{FirstName} {LastName} is the {JobTitle} at {CompanyName}",
"L" or "LOGIN" => $"{FirstName}.{LastName}",
"UD" or "USERDOMAIN" => $"{FirstName}.{LastName}@{CompanyName}.local",
_ => throw new FormatException($"Invalid format {format}")
};
}
Note: I decided to make the keywords work for both uppercase and lowercase inputs. To do this, I converted format
to an uppercase string using ToUpper
.
var employee = new Employee()
{
FirstName = "John",
LastName = "Smith",
JobTitle = "CEO",
CompanyName = "BMW"
};
Console.WriteLine(employee); // John Smith is the CEO at BMW
Console.WriteLine(employee.ToString()); // John Smith is the CEO at BMW
Console.WriteLine(employee.ToString("G")); // John Smith is the CEO at BMW
Console.WriteLine(employee.ToString("Full")); // John Smith is the CEO at BMW
Console.WriteLine(employee.ToString("L")); // John.Smith
Console.WriteLine(employee.ToString("LOgin")); // John.Smith
Console.WriteLine(employee.ToString("Ud")); // [email protected]
Console.WriteLine(employee.ToString("UserDomain")); // [email protected]
Dynamic Approach
This approach gives users the flexibility to include multiple tokens in a single format. For example, a user can specify the first name, initial of the middle name, job title, etc., all within the same string.
public class Person : IFormattable
{
public required string FirstName { get; set; }
public string? MiddleName { get; set; }
public required string LastName { get; set; }
public override string ToString()
{
return ToString("G");
}
public string ToString(string? format)
{
return ToString(format, CultureInfo.CurrentCulture);
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
// F. LL
// FF M. LL
// F,M LL
// G => full
if (string.IsNullOrEmpty(format))
{
format = "G";
}
Dictionary<string, string> mapper = new Dictionary<string, string>()
{
{ "F", FirstName[0].ToString() },
{ "FF", FirstName },
{ "M", (MiddleName ?? "")[0].ToString() },
{ "MM", MiddleName ?? "" },
{ "L", LastName[0].ToString() },
{ "LL", LastName },
{ "G", $"{FirstName} {MiddleName} {LastName}" }
};
// FF M. L
StringBuilder sb = new StringBuilder();
for (int i = 0; i < format.Length; i++)
{
char c = format[i]; // F
string token = c.ToString(); // F
while (i + 1 < format.Length && c == format[i + 1])
{
token += c;
i++;
}
if (mapper.TryGetValue(token, out var value))
{
sb.Append(value);
}
else
{
sb.Append(token);
}
}
return sb.ToString();
}
}
Here’s a breakdown of the code:
First, we create a dictionary of tokens mapped to their respective values. For example, F represents the initial of the first name, and FF represents the full first name.
Then, we loop through the format string, checking each character for matching tokens. We also handle cases where a token could be part of a longer token (e.g., F vs FF).
Once we identify a token, we retrieve its value from the dictionary and append it to a StringBuilder
.
Testing this approach gives us the following output:
var person = new Person()
{
FirstName = "Jack",
MiddleName = "John",
LastName = "Smith"
};
Console.WriteLine(person.ToString("G")); // Jack John Smith
Console.WriteLine(person.ToString("FF M. LL")); // Jack J. Smith
Console.WriteLine(person.ToString("FM LL")); // JJ Smith
Flexible Approach
This approach provides even more flexibility by allowing users to append external text to the format string without disrupting the output. For instance, if we try formatting a string like this, it will break:
var person = new Person()
{
FirstName = "Jack",
MiddleName = "John",
LastName = "Smith"
};
Console.WriteLine(person.ToString("Man, L is the LOSER"));
// Jan, S is the SOSER
Notice how the M in “Man” was replaced by the middle name initial, and the L in “Loser” was replaced by the last name initial. To solve this, we can use a different method by wrapping tokens in curly braces {X}
.
public string ToString(string? format, IFormatProvider? formatProvider)
{
// F. LL
// FF M. LL
// F,M LL
// G => full
if (string.IsNullOrEmpty(format))
{
format = "{G}";
}
Dictionary<string, string> mapper = new Dictionary<string, string>()
{
{ "{F}", FirstName[0].ToString() },
{ "{FF}", FirstName },
{ "{M}", (MiddleName ?? string.Empty)[0].ToString() },
{ "{MM}", MiddleName ?? string.Empty },
{ "{L}", LastName[0].ToString() },
{ "{LL}", LastName },
{ "{G}", $"{FirstName} {MiddleName} {LastName}" }
};
foreach (var key in mapper.Keys)
{
format = format.Replace(key, mapper[key]);
}
return format;
}
Now, instead of iterating through each character in the format string as an array, we loop through the dictionary keys and replace each key in the format string with its corresponding value.
Console.WriteLine(person.ToString("Man, {F}.{L}. is the LOSER"));
// Man, J.S. is the LOSER
With these techniques, you can add custom formatting to your classes and make your code more flexible and easy to use.
Happy Coding!