2
\$\begingroup\$

I often need a simple exception with a meaningful name and some helpful message which usually just looks like this one:

public class BackgroundImageNotFoundException : Exception
{
  public BackgroundImageNotFoundException(string fileName)
  : base($"Where is the '{fileName}' image?")
  { }
}

Then I end up creating a bunch of similar classes that mostly differ only by name. I thought there must be some more efficient way to do this so I've been experimenting with a new idea that I call the DynamicException. It is a special exception that gets compiled at runtime and contains only the most basic properties.


At the bottom of it is the main DynamicExceptionFactory class which creates a new excption type at runtime. Such exceptions have the specified name, message and/or inner exception:

public interface IDynamicExceptionFactory
{
    [NotNull, ContractAnnotation("name: null => halt")]
    Exception CreateDynamicException([NotNull] string name, [CanBeNull] string message, [CanBeNull] Exception innerException);
}

internal class DynamicExceptionFactory : IDynamicExceptionFactory
{
    public static IDynamicExceptionFactory Default { get; } = new DynamicExceptionFactory();

    public Exception CreateDynamicException(string name, string message, Exception innerException)
    {
        if (name == null) throw new ArgumentNullException(nameof(name));
        if (!name.EndsWith(nameof(Exception))) throw new ArgumentException(paramName: nameof(name), message: $"Exception name must end with '{nameof(Exception)}'.");

        var baseType = typeof(DynamicException);
        var baseConstructorParameterTypes = new[] { typeof(string), typeof(Exception) };
        var baseConstructor = baseType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, baseConstructorParameterTypes, null);

        var assemblyName = new AssemblyName($"DynamicAssembly_{Guid.NewGuid():N}");
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
        var typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public);
        typeBuilder.SetParent(typeof(DynamicException));

        // Create a constructor with the same number of parameters as the base constructor.
        var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, baseConstructorParameterTypes);

        var ilGenerator = constructor.GetILGenerator();

        // Generate constructor code
        ilGenerator.Emit(OpCodes.Ldarg_0);                // push 'this' onto stack.
        ilGenerator.Emit(OpCodes.Ldarg_1);                // push 'message' onto stack.
        ilGenerator.Emit(OpCodes.Ldarg_2);                // push 'innerException' onto stack.
        ilGenerator.Emit(OpCodes.Call, baseConstructor);  // call base constructor

        ilGenerator.Emit(OpCodes.Nop);                    // C# compiler add 2 NOPS, so
        ilGenerator.Emit(OpCodes.Nop);                    // we'll add them, too.

        ilGenerator.Emit(OpCodes.Ret);                    // Return

        var dynamicExceptionType = typeBuilder.CreateType();
        return (Exception)Activator.CreateInstance(dynamicExceptionType, message, innerException);
    }
}

Custom exceptions are inherited from a DynamicException so that they can be caught and filtered.

public abstract class DynamicException : Exception
{
    protected DynamicException(string message, Exception innerException)
        : base(message, innerException) { }

    [NotNull]
    public static IDynamicExceptionFactory Factory => DynamicExceptionFactory.Default;
}

In order to make the exception creation really simple, I use a few helper extensions for tuples that I use to specify some or all of the three parameters:

public static class DynamicExceptionFactoryExtensions
{
    /// <summary>
    /// Creates a DynamicException from the specified template.
    /// </summary>
    [NotNull, ContractAnnotation("factory: null => halt; template: null => halt")]
    public static Exception CreateDynamicException([NotNull] this IDynamicExceptionFactory factory, [NotNull] IDynamicExceptionTemplate template)
    {
        if (factory == null) throw new ArgumentNullException(nameof(factory));
        if (template == null) throw new ArgumentNullException(nameof(template));

        return factory.CreateDynamicException(template.Name(), template.Message, template.InnerException);
    }

    /// <summary>
    /// Creates a DynamicException with the name of the calling method, and with the specified message and optionally an inner exception.
    /// </summary>
    /// <returns></returns>
    [NotNull, ContractAnnotation("factory: null => halt")]
    public static Exception CreateDynamicException([NotNull] this IDynamicExceptionFactory factory, string message, Exception innerException = null, [CallerMemberName] string memberName = null)
    {
        if (factory == null) throw new ArgumentNullException(nameof(factory));

        return factory.CreateDynamicException($"{memberName}{nameof(Exception)}", message, innerException);
    }

    public static Exception ToDynamicException(this (string Name, string Message) template)
    {
        return DynamicException.Factory.CreateDynamicException(template.Name, template.Message, null);
    }

    public static Exception ToDynamicException(this (Enum ErrorCode, string Message) template)
    {
        return DynamicException.Factory.CreateDynamicException(template.ErrorCode.ToString(), template.Message, null);
    }

    public static Exception ToDynamicException(this (string Name, string Message, Exception InnerException) template)
    {
        return DynamicException.Factory.CreateDynamicException(template.Name, template.Message, template.InnerException);
    }

    public static Exception ToDynamicException(this (Enum ErrorCode, string Message, Exception InnerException) template)
    {
        return DynamicException.Factory.CreateDynamicException(template.ErrorCode.ToString(), template.Message, template.InnerException);
    }
}

Exception filtering is supported by another set of extensions to check the exception name:

public static class DynamicExceptionExtensions
{
    [ContractAnnotation("ex: null => halt; name: null => halt")]
    public static bool NameEquals([NotNull] this DynamicException ex, [NotNull] string name, StringComparison comparisonType = StringComparison.Ordinal)
    {
        if (ex == null) throw new ArgumentNullException(nameof(ex));
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
        return ex.GetType().Name.Equals(name, comparisonType);
    }

    [ContractAnnotation("ex: null => halt")]
    public static bool NameEquals([NotNull] this DynamicException ex, Enum errorCode, StringComparison comparisonType = StringComparison.Ordinal)
    {
        if (ex == null) throw new ArgumentNullException(nameof(ex));
        return ex.NameEquals(errorCode.ToString(), comparisonType);
    }

    [ContractAnnotation("ex: null => halt")]
    public static bool CreatedFrom<T>([NotNull] this DynamicException ex, StringComparison comparisonType = StringComparison.Ordinal) where T : IDynamicExceptionTemplate
    {
        if (ex == null) throw new ArgumentNullException(nameof(ex));
        return ex.NameEquals(Regex.Replace(typeof(T).Name, "Template$", string.Empty), comparisonType);
    }        
}

For exceptions that can be used more than once there is an option to create an exception template:

public interface IDynamicExceptionTemplate
{
    [CanBeNull]
    string Message { get; }

    [CanBeNull]
    Exception InnerException { get; }
}

public abstract class DynamicExceptionTemplate : IDynamicExceptionTemplate
{
    public abstract string Message { get; }

    public Exception InnerException { get; set; }

    [NotNull, ContractAnnotation("template: null => halt")]
    public static implicit operator Exception([NotNull] DynamicExceptionTemplate template)
    {
        if (template == null) throw new ArgumentNullException(nameof(template));
        return template.ToDynamicException();
    }
}

public static class DynamicExceptionTemplateExtensions
{
    public static string Name<T>(this T template) where T : IDynamicExceptionTemplate
    {
        return Regex.Replace(typeof(T).Name, $"({nameof(Exception)})?Template$", nameof(Exception));
    }

    public static Exception ToDynamicException(this IDynamicExceptionTemplate template)
    {
        return DynamicException.Factory.CreateDynamicException(template.Name(), template.Message, template.InnerException);
    }
}

Such a template contains only a message that is rendered from custom properties (if any) and the name of the exception is derived form the template's name.


Example

Here's an example on how I use this:

throw ("SettingNotFoundException", $"Setting {fullName.ToString().QuoteWith("'")} not found.").ToDynamicException());

If I wanted to use it in more than one place I could create a template like:

public class SettingNotFoundExceptionTemplate : DynamicExceptionTemplate
{
    public override string Message => $"Setting {SettingName.QuoteWith("'")} not found.";
    public string SettingName { get; set; }
}

and throw it with:

throw new SettingNotFoundExceptionTemplate
{
    SettingName = fullName.ToString()
}
.ToDynamicException();

In tests I can check if it gets thrown with:

Assert.That.ThrowsExceptionFiltered<DynamicException>(() =>
{
    // code that throws a SettingNotFoundException
},
ex => ex.NameEquals("SettingNotFoundException"));

A templated exception could be filtered with:

ex.CreatedFrom<SettingNotFoundExceptionTemplate>()

where ThrowsExceptionFiltered is my Assert extension allowing exception filtering:

public static T ThrowsExceptionFiltered<T>(this Assert assert, Action action, Func<T, bool> filter = null) where T : Exception
{
    filter = filter ?? (ex => true);

    try
    {
        action();
        Assert.Fail($"Expected exception {typeof(T).Name.QuoteWith("'")}, but none was thrown.");
    }
    catch (T ex) when (filter(ex))
    {
        return ex;
    }
    catch (Exception ex)
    {
        Assert.Fail($"Expected exception '{typeof(T).Name}', but {ex.GetType().Namespace.QuoteWith("'")} was thrown.");
    }

    // This is only to satisfy the compiler. We'll never reach to this as it'll always fail or return earlier.
    return null;
}
\$\endgroup\$
12
  • 2
    \$\begingroup\$ I suspect you could do something similar with a custom exception which also holds the Error enum value; given that you alway catch the base exception and compare on the enum. \$\endgroup\$
    – hocho
    Oct 5, 2017 at 4:37
  • \$\begingroup\$ @hocho not quite, a custom exception would always be visible as CustomException when you log it because it calls ex.ToString() so you'll always have to look for the enum property to know what went wrong. What I want is to have the reason for the exception where it belongs which is the name of the exception like SettingNullException rather than CustomException.Error = "SettingNull". \$\endgroup\$
    – t3chb0t
    Oct 5, 2017 at 8:13
  • \$\begingroup\$ if naming for logging is the main concern, one can override the ToString() of the custom exception to return the desired output. \$\endgroup\$
    – hocho
    Oct 5, 2017 at 16:20
  • \$\begingroup\$ @hocho manipulating the string seems like a really bad idea ;-] if you however meant creating the string from scratch, it would be even worse. \$\endgroup\$
    – t3chb0t
    Oct 8, 2017 at 13:03
  • 2
    \$\begingroup\$ In my very personal opinion, I find dynamism rarely helpful in compiled languages/environments including C#/.NET. I think I understand what you're trying to achieve in terms of readability, code tersity, and avoiding type proliferation. I'm just not seeing how using this "heavy artillery" is an adequate way even if that complexity is isolated in a single class. Since it's working as stringly-based, why not just use a naive implementation of a DynamicException which would have some kind of read-only property (string ExceptionKind)? \$\endgroup\$ Oct 8, 2017 at 17:40

3 Answers 3

11
\$\begingroup\$

Surprising

This is a gross violation of the Principle of Least Surprise. The only possible reaction I could have seeing this in the wild would be, "What the heck?" This is not a normal way of using exceptions. New developers approaching the code are likely to have a similar reaction.

One particular thing that would trip me up and force me to spend time investigating what in the world is going on is this code:

throw ("SettingNotFoundException", $"Setting {fullName.ToString().QuoteWith("'")} not found.").ToDynamicException());

During a quick skim, I'm unlikely to see the ToDynamicException call, which makes me stop and wonder if you somehow introduced new syntax to the language.

You should also note that your design has forced you to basically re-implement or wrap a lot of the standard exception functionality (e.g., filtering). Needing to re-implement something that was already done for you is a good indicator that you're going outside the normal way of using your tools.

Is this really an improvement?

I'm not sure how your code is actually any better than the standard approach. You already found you had to create a "template" class to achieve what you were after:

Yours:

public class SettingNotFoundExceptionTemplate : DynamicExceptionTemplate
{
    public override string Message => $"Setting {SettingName.QuoteWith("'")} not found.";
    public string SettingName { get; set; }
}

But judicious use of standard exception patterns and constructors doesn't seem any worse than your class here:

public class SettingNotFoundException : Exception
{
    public string SettingName { get; }

    public SettingNotFoundException(string settingName, Exception innerException = null)
        : base($"Setting {SettingName.QuoteWith("'")} not found.", innerException)
    {
        this.SettingName = settingName;
    }
}

Yours saves about 3 lines of code at the expense of being unable to write catch(SettingNotFoundException) and normal looking throws. Seems like a bad trade off to me.

Your test assertions aren't really an improvement over existing frameworks, either. Assuming you're using NUnit 3, then combined with the exception type above, you can use Throws and AreEqual for your example:

var ex = Assert.Throws<SettingNotFoundException>(
    () =>
    {
        // code that throws a SettingNotFoundException
    }
);
Assert.AreEqual("expectedSettingName", ex.SettingName);

If you don't want to have several files for a set of exceptions, feel free to establish a standard of centralizing exceptions into a single file. While a little unorthodox, this won't lead to the kind of confusion and extra time spent trying to understand your code that your approach will.

Why do you need so many Exception classes?

I gather that your SettingNotFoundException is only an example and that you are using this code to generate many exception types. But this begs the question of why you're creating so many exceptions that what I describe above is overly burdensome.

You mentioned in chat:

You should already know what [happened] by not even reading the message.

This is not how C# was designed to work, so you need to let this go if you're going to write C# code. It's more important that your code is easy to understand and work with for other developers than it is for your code to be in line with your ideal. No language is ideal; they're all going to have some approaches and norms you don't like.

It also looks like you might be trying to have a different exception for all the different situations you need to assert on in your tests. This also isn't how exceptions are normally used, so this appears to be an example of "test induced design damage."

You normally have a small number of general exceptions that are reused throughout a library of classes, and then you rarely have a couple more specific ones that match up to very specific and situational catch blocks.

In other words, you need to rethink your approach of creating a ton of exception types. You won't be catching most of them explicitly outside of your tests, and the ones you don't are very unlikely to be actually needed. Your tests will be fine asserting on less specific exceptions. If you feel it's absolutely necessary to distinguish from other causes, you might perform a limited assertion on the message (such as a string contains check), but even this has questionable value. You might also consider organizing your code under test differently, in such a way that it's impossible or very unlikely for it to throw an exception for the wrong reason, to reduce your risk without creating all this cognitive overhead.

Norms and Standards

The bottom line is that there are already existing norms and standards that will reduce the amount of work you're trying to avoid. Find ways to use them, instead of trying to revise how the language works. You need to let go of your notions of how a language should work and let the language work in the way it was designed to.

\$\endgroup\$
3
  • \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$
    – jpmc26
    Apr 23, 2019 at 15:24
  • 1
    \$\begingroup\$ I was going to comment on many aspects of this answer and the many misconceptions it presents about how you are supposed to work with your programming language. But I'll say just this (CC: everyone): open your mind and let your code/langauge work for you - challange norms and standards, especially when they cause you additional work. Break known rules/standards and make new ones. By the end of the day what counts is that our software is easier to maintain and debug! If you have good reasons for doing something in a non-standard way then stick to it! Conservatism will only slow you down! \$\endgroup\$
    – t3chb0t
    Apr 25, 2019 at 6:22
  • \$\begingroup\$ @t3chb0t I do challenge norms. Like OOP approaches, heavyweight ORMs, dependency injection. If a practice is actively hurting the quality of my code, I won't use it. But if it isn't or I don't have a better idea, I do everything I can to figure out a way to be effective without breaking the norm. \$\endgroup\$
    – jpmc26
    Apr 25, 2019 at 13:13
1
\$\begingroup\$

It turned out that the only thing there was missing was a cache.

private readonly ConcurrentDictionary<string, Type> _cache = new ConcurrentDictionary<string, Type>();

public Type GetDynamicExceptionType(string name)
{
    if (name == null) throw new ArgumentNullException(nameof(name));
    return _cache.GetOrAdd(name, CreateDynamicExceptionType);
}

In some rare cases when an exception is thrown too often (usually a bug with a try/catch in a loop) there was a performance hit.

Other then that I already use it multiple times in many applications and being able to create ad-hoc exceptions is a real time saver when coding as well as during debugging by more precise excepitons and messages ;-)

\$\endgroup\$
1
\$\begingroup\$

I find it very cool that after a year and a half people take interest in this mini experiment which appears to be extremely controversial. I had never thought that the new question about a similar utility for / would make such a huge impact.

I appriciate the upvotes and all the feedback and I'm very surprised and somehow disappointed too that there are also so many downvotes just because it's something unexpected, something that you've probably have never thought of before.


This wiki-entry should clarify some of the questions that I've been asked in the comments but here is not enough space to address all the answers.


You're asking me why I would want to use this utility, so here are some more details why I find it's such a great help:

  • Standard exceptions are not helpful becasue they contain mostly only generic information.
  • Creating helpful exceptions is a tedious taks because you have to create many classes that carry appropriate names. OK, but you're saying that there are many predefined ones that one can reuse. Sure, you could but it would either be too unspecific or ends as throw Exception(...) because you are tired of creating the 10th custom exception using the same boilerplate code over and over agian.

And this is the root of the problem. You cannot create a nice hierarchy of exceptions where each level is adding some new information to the context because you'd need to create countles classes if you wanted to have this nice hierarchy:

SaveCartExcetpion - could not save cart for user z
    UpdateProductPriceException - could not change price to 10€
        GetProductException - could not get product y
            SqlException - generic message...

When you see something like this in your logs, you instantly know what went wrong and don't have to search for hours what actually happened or what the workflow was. You also don't have to study the stack-trace to find where it came from or what action caused it.

It's a huge timesaver that doesn't cost you a penny becuase all you have to do is to call throw DynamicException.Create("name", "message");, done.

vs the classic-way where you create a reusable exception (because otherwise it's simply a lot of copy/paste & rename etc work) like OperationFailedException that now becomes useless agian because it doesn't carry any helpful information in its name at all:

OperationFailedException - could not save cart for user z
    OperationFailedException - could not change price to 10€
        OperationFailedException - could not get product y
            SqlException - generic message...

Many of you believe or have heard that exceptions are thrown in order to be caught and handled. This is some strange myth that is blindly repeated. There is nothing you can do about 99 or 100 of them so it is not necessasry to create exceptions catchable by their type and with the new when filter I can catch them anyway, if I wanted, with

catch(DynamicException ex) when ex.NameStartWith("UpdateProduct")

There are only two pieces of information that an exception can hold: its name and the message.

Anything else is pointless because it requries additional logger configuration or other ToString or Message overrides.

This is why the name of the exception should tell you as precisely as possible what happened and the message either gives out some additional information or tells you what to do in order to fix it (if possible).


I've been using excepitons the classic-way long enough to know that you either don't use them at all or they are of absolutely no value when things go sideways.


I'm not trying to convice anybody to use this and it wasn't my intention at any time. You can either believe me or not that it is a great timesaver. I don't care. All I care about is code improvement.


The bottom line is: I'm not going to write code that can generate themslef.

\$\endgroup\$
4
  • 3
    \$\begingroup\$ I would consider SaveCartException, UpdateProductPriceException and GetProductException to all be too specific and unnecessary exceptions. Exceptions doesn't give information about what went wrong in it's name, but rather give the information about why it went wrong. File not found, illegal argument, null pointer, SQL error, all these are why. The message and the stack-trace is for what and where. \$\endgroup\$ Apr 23, 2019 at 19:25
  • \$\begingroup\$ @SimonForsberg this is what the deepest exception tells you. The other ones are on a different level and their reason for the exception is also different. But this additional context is what makes debugging such code so easy because you know exactly what happened without spending hours with the debugger. \$\endgroup\$
    – t3chb0t
    Apr 24, 2019 at 3:28
  • \$\begingroup\$ I've examined your answer, and I still see no advantage to the dynamic aspect. The primary value of your custom exceptions is that they put the reader in the mindset that the exception is coming from the project code, not a third party. But you could achieve the same thing with general CartException and ProductException, or maybe even ThisProjectException or OurValidationException, classes. Beyond that, they don't really communicate any details about why an error occurred, especially since the actual bug could be elsewhere; they just communicate some data in the message. \$\endgroup\$
    – jpmc26
    Apr 25, 2019 at 13:52
  • \$\begingroup\$ @jpmc26 in order to see advantage in something that makes your work more efficient you first have to do it inefficiently. Thus, many people won't recognize this tool as a timesaver because they have never used any meaninungful exception before. If they had, then they would have to have created a lot of them and be fed up with writing the same bilerplate code over and over again. Like those who created EF because they didn't want to write SQL anymore (or at least avoid it in a lot of situations). \$\endgroup\$
    – t3chb0t
    Apr 25, 2019 at 14:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.