runtime: Proposal: Convenience API for throwing exceptions
Background
It’s quite verbose to validate arguments because you have to write an if-statement and throw new XXXException
for each validation. Even with C# 7’s new throw-expressions, you still have to write out the latter part. This makes people write their own helper classes for validating arguments, e.g. here or here. It would be nice if we had such a helper class as part of the framework.
Proposal
namespace System
{
public static class Verify
{
public static void Argument(bool condition, string message, string argumentName);
public static void InRange(bool condition, string argumentName);
public static void InRange(string s, int index, int count, string sName, string indexName, string countName);
public static void InRange<T>(T[] array, int index, int count, string arrayName, string indexName, string countName);
public static void NotEmpty<T>(IEnumerable<T> source, string sourceName);
public static void NotNegative(int argument, string argumentName);
public static void NotNull(bool condition, string argumentName);
public static void NotNull<T>(T argument, string argumentName) where T : class;
public static void Positive(int argument, string argumentName);
}
}
// Sample usage:
T MyGenericMethod<T>(T[] array, int index, int count)
{
// Note: All of this is equivalent to Verify.InRange(array, index, count, nameof(array), nameof(index), nameof(count)).
// The arguments are validated manually for demo purposes.
Verify.NotNull(array, nameof(array));
Verify.NotNegative(index, nameof(index));
Verify.NotNegative(count, nameof(count));
Verify.InRange(array.Length - index >= count, nameof(index));
}
A sample implementation can be found here.
Remarks
-
In my experience, it’s common to validate things like a signed integer being positive/nonnegative, so those patterns will get their own
Positive
/NotNegative
methods. This will enable us to provide a better error message if such a check fails. These methods throw the same exception type asInRange
.- Same applies for
InRange
,NotEmpty
- Same applies for
-
The class will work nicely with
using static
:
using static System.Verify;
T MyGenericMethod<T>(T[] array, int index, int count)
{
NotNull(array, nameof(array));
NotNegative(index, nameof(index));
NotNegative(count, nameof(count));
InRange(array.Length - index >= count, nameof(index));
}
- The extra
NotNull
overload taking abool
covers the rare cases when people are writing generic code, and the parameter might be null but the compiler can’t guarantee that it’s a class. e.g. Like here. It also covers the rare times when someone would want to verify a nullable is non-null.- I didn’t include a
NotNull<T>(T?, string) where T : struct
nullable overload since if someone thinks a nullable is non-null they would likely 1) not accept a nullable parameter in the first place, or 2) if they’re calling some method they know returns a non-null nullable, they’re likely to cast theT?
to aT
or call.Value
, which do the validation automatically, instead of bothering to validate themselves.
- I didn’t include a
This is a follow-up to https://github.com/dotnet/corefx/issues/12509 after putting some thought into the API.
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 7
- Comments: 29 (22 by maintainers)
It would likely need to be something like NuGet “source” package when used; rather than a built in to runtime as assembly, so the tests will inline across assembly boundaries when compiled AoT/R2R
So, I’ve had time to think about it. We already have Debug and Trace, so the concern about strictly following the FDG with regards to using verbs for class names is probably not a concern here. So Require is probably an “OK” name. However, I still believe Argument to be superior.
I like the fact that with Argument as the name the assertions read exactly the same as the exceptions they throw: Argument.NotNull throws ArgumentNullException.
I also just noticed in your sample implementation you have overloads of InRange for validating indexes, but they throw ArgumentException. I’ve always thrown ArgumentOutOfRange exception which made more sense to me. However, looking at List<T>.BinarySearch it looks like precedent says otherwise? List<T>.BinarySearch also throws ArgumentOutOfRangeException but that’s for validation that index and count are non-negative, while it just throws ArgumentException for range violations (confusing to me). You also don’t assign a ParamName to the exception, which is something that really bothers me, but, again, this is the behavior of List<T>.BinarySearch as well. So, while you’ve followed precedent, maybe we should consider changing some of this behavior?
Also you only have overloads for string and arrays when you should probably just have a single generic overload constrained for IList<T> types. Also, it might be nice to have an overload that doesn’t take the count parameter and instead just validates index is in the range of valid indexes for the collection.
BTW, while I’d like to see something like this regardless, these helpers really don’t shine until we get your language proposal. I vote +1 for both.
“Chaining” is problematic. The example given of
value.VerifyNotNull().VerifyNotEmpty()
requires these verifiers to be extension methods that work on all types. I find that repugnant, to be honest. It pollutes IntelliSense. There is a solution to this, which I use in my Testify unit testing library. The above can be coded asArgument.Verify(value).NotNull().NotEmpty()
, whereArgument.Verify
returns some unique wrapper type, such as anArgumentValue<T>
type, and the extension methods are based on that type. This also has the benefit of making it easy to add assertions and even to limit IntelliSense to valid types (so, for instance,NotNull
won’t be in IntelliSense for value types). The downside is in performance and possibly memory/gc overhead ifArgumentValue<T>
is a reference type. Also, for this to work with the concept of returning the value the API is more complex with the need to chain to aValue
property, or to rely on implicit conversions.All that said, however, you could always take the functional POV to chaining and code it as
NotNull(NotEmpty(value))
(with lots more syntactical noise without the compiler feature to get at the caller expression and without a static using).While these verification methods are handy (I use similar often myself), there is something to be said about the benefit of a throw within a method for static analysis.
Changing
ErrorContext.Error
in Microsoft.CSharp from throwing to returning an exception to throw, and passing that change up to any method that always threw, made quite a few chunks of code not only more clearly unreachable, but demonstrably unreachable so the compiler would complain about their being there. It was a big jump in how easy it was to reason about a lot of the methods.This is going to become even more pertinent when C#8 introduces nullability checks as promised:
or even
vs
Unless we get a way to flag to the compiler that
Requres.NotNull()
will never succeed with with a null argument, and hence thatsomeArg
is demonstrably not null after it, using it will lose the nullability checking of the language.How do we get to have both?
There is a need for APIs like that in corfxlab and in the platfrom. For example, System.Slices already duplicates this: https://github.com/dotnet/corefxlab/blob/master/src/System.Slices/System/Runtime/Contract.cs
I never turned the Contract.cs APIs into a package as there are several interesting trade-offs here, and I did not have time to think how to resolve them.
Also, I think this is related to a feature that @justinvp was pushing at some point (and I liked a lot), i.e. the ability to throw various common exceptions conveniently without the need to deal with message resources.
Lastly, I would love to have a set of attributes for identifying exceptions that propagate out of APIs. The attributes would divide errors into usage errors (roughly preconditions) and runtime errors (errors that need to be handled). Usage errors would show in VS tooltips and runtime error attributes would be used by analyzers to detect code that does not handle them properly.