Beware exceptions in attribute constructors


If you’d like to have some really wacky bugs, be sure to do something like this:

public class BlowupAttribute : Attribute
{
    public BlowupAttribute(int time)
    {
        if (time <= 0)
            throw new ArgumentOutOfRangeException("time", time, "Must be greater than zero.");
    }
}

Attributes are a little different than other classes, as you’re never really in control of when the constructor gets called.  Things get especially weird in unit tests, where test runners do quite a bit of reflection that triggers the constructors of attributes.  This test fails:

[Blowup(-1)]
public void Asplode()
{
}

[Test]
public void Fails_anyway()
{
    true.ShouldBeTrue();
}

Simply because another member in the class has a bad value in the attribute.  Unfortunately, the stack trace gives absolutely zero hint on where the exception occurred:

TestCase 'M:AttributeExceptions.Blarg.Fails_anyway'
failed: Must be greater than zero.
Parameter name: time
Actual value was -1.
    System.ArgumentOutOfRangeException: Must be greater than zero.
    Parameter name: time
    Actual value was -1.
    C:devMSTestSpecMSTestSpec.TestsAttributeExceptions.cs(13,0): at AttributeExceptions.BlowupAttribute..ctor(Int32 time)
    at System.Reflection.CustomAttribute._CreateCaObject(Void* pModule, Void* pCtor, Byte** ppBlob, Byte* pEndBlob, Int32* pcNamedArgs)
    at System.Reflection.CustomAttribute.CreateCaObject(Module module, RuntimeMethodHandle ctor, IntPtr& blob, IntPtr blobEnd, Int32& namedArgs)
    at System.Reflection.CustomAttribute.GetCustomAttributes(Module decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType, Boolean mustBeInheritable, IList derivedAttributes)
    at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeMethodInfo method, RuntimeType caType, Boolean inherit)
    at NUnit.Core.Reflect.HasAttribute(MemberInfo member, String attrName, Boolean inherit)
    at NUnit.Core.Reflect.GetMethodWithAttribute(Type fixtureType, String attributeName, BindingFlags bindingFlags, Boolean inherit)
    at NUnit.Core.NUnitTestFixture..ctor(Type fixtureType)
    at NUnit.Core.Builders.NUnitTestFixtureBuilder.MakeSuite(Type type)
    at NUnit.Core.Builders.AbstractFixtureBuilder.BuildFrom(Type type)
    at NUnit.Core.Extensibility.SuiteBuilderCollection.BuildFrom(Type type)
    at NUnit.Core.Builders.TestAssemblyBuilder.GetFixtures(Assembly assembly, String ns)
    at NUnit.Core.Builders.TestAssemblyBuilder.Build(String assemblyName, Boolean autoSuites)
    at NUnit.Core.Builders.TestAssemblyBuilder.Build(String assemblyName, String testName, Boolean autoSuites)
    at NUnit.Core.TestSuiteBuilder.BuildSingleAssembly(TestPackage package)
    at NUnit.Core.TestSuiteBuilder.Build(TestPackage package)
    at NUnit.AddInRunner.NUnitTestRunner.run(ITestListener testListener, Assembly assembly, ITestFilter filter)
    at NUnit.AddInRunner.NUnitTestRunner.runMethod(ITestListener testListener, Assembly assembly, MethodInfo method)
    at NUnit.AddInRunner.NUnitTestRunner.RunMember(ITestListener testListener, Assembly assembly, MemberInfo member)
    at TestDriven.TestRunner.AdaptorTestRunner.Run(ITestListener testListener, ITraceListener traceListener, String assemblyPath, String testPath)
    at TestDriven.TestRunner.ThreadTestRunner.Runner.Run()

Nowhere do we see where the original attribute was declared, that information is lost.  Attributes are meant for metadata, and exceptions can really screw things up.  To be safe, avoid throwing exceptions in an attribute constructor.  Also, be very careful of any complex operations done in the constructor.  If you’re doing anything more than merely capturing parameters passed in, you’re doing too much.

Well that’s just precious