Because I don't like writing the same useless exeption messages all the time ;-) I thought I create a better exception that would make this for me. This is what I came up with:
[Serializable]
public class SmartException : Exception
{
private const string PropertyNamePrefix = "$";
private const string InnerExceptionSeparator = " >>> ";
protected SmartException() { }
protected SmartException(Exception innerException) : base(null, innerException) { }
public override string Message => FormatMessage();
protected T GetValue<T>([CallerMemberName] string propertyName = "")
{
return (T)Data[PropertyNamePrefix + propertyName];
}
protected void SetValue<T>(T value, [CallerMemberName] string propertyName = "")
{
Data[PropertyNamePrefix + propertyName] = value;
}
private string FormatMessage()
{
var ex = (Exception)this;
var message = new StringBuilder(1024);
while (ex != null)
{
var exceptionSeparator = message.Length > 0 ? InnerExceptionSeparator : string.Empty;
var exceptionName = Regex.Replace(ex.GetType().Name, "Exception$", string.Empty);
message.Append($"{exceptionSeparator}{exceptionName}");
var properties = FormatProperties(ex);
message.Append(string.IsNullOrEmpty(properties) ? null : $": {properties}");
ex = ex.InnerException;
}
return message.ToString();
}
private static string FormatProperties(Exception ex)
{
return
ex is SmartException
? FormatPropertiesFromData(ex)
: FormatPropertiesFromType(ex);
}
private static string FormatPropertiesFromData(Exception ex)
{
var entries = ex.Data
.Cast<DictionaryEntry>()
.Where(de =>
de.Key is string &&
((string)de.Key).StartsWith("$") &&
de.Value != null);
var properties = new StringBuilder(1024);
foreach (var entry in entries)
{
var separator = properties.Length > 0 ? " " : string.Empty;
var propertyName = Regex.Replace(
entry.Key.ToString(),
$"^[{PropertyNamePrefix}]",
string.Empty);
properties.Append($"{separator}{propertyName} = \"{entry.Value}\"");
}
return properties.ToString();
}
private static string FormatPropertiesFromType(Exception ex)
{
var propertyInfos =
ex.GetType()
.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.Name != "Message");
var properties = new StringBuilder(1024);
foreach (var propertyInfo in propertyInfos)
{
var value = propertyInfo.GetValue(ex);
if (value == null)
{
continue;
}
var separator = properties.Length > 0 ? " " : string.Empty;
properties.Append($"{separator}{propertyInfo.Name} = \"{value}\"");
}
return properties.ToString();
}
}
Usage
public class UserNotFoundException : SmartException
{
public UserNotFoundException(Exception innerException) : base(innerException) {}
public string UserName
{
get { return GetValue<string>(); }
set { SetValue(value); }
}
}
and for:
var innerEx = new FileNotFoundException(null, "helloworld.txt");
var ex = new UserNotFoundException(innerEx) { UserName = "TestUser" };
the message would be:
UserNotFound: UserName = "TestUser" >>> FileNotFound: FileName = "helloworld.txt"
What it does is cutting-off the Exception from the name of the exception type and appending the properties to the message together with all inner exceptions.