Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: ArgumentException helper methods #77749

Closed
Tracked by #79053
geeknoid opened this issue Nov 1, 2022 · 13 comments · Fixed by #86007
Closed
Tracked by #79053

[API Proposal]: ArgumentException helper methods #77749

geeknoid opened this issue Nov 1, 2022 · 13 comments · Fixed by #86007
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Milestone

Comments

@geeknoid
Copy link
Member

geeknoid commented Nov 1, 2022

EDIT 5/1/2023 by @stephentoub

The proposal suggested four methods:

  • ThrowIfNullOrWhitespace(string, ...)
  • ThrowIfNullOrEmpty(string, ...)
  • ThrowIfNullOrEmpty(IEnumerable, ...)
  • ThrowIfBufferTooSmall(int, int, ...)

The second already exists, the third is too expensive in its proposed form, and more validation would be needed on the fourth to conclude exactly what form it would take (e.g. someone trying it across .NET runtime and seeing what issues we might hit).

As such, I'm narrowing this down to just the first (which we've previously considered and rejected, but we've since received enough +1s that it's worth reconsidering):

public class ArgumentException
{
    // existing
    public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);

+   public static void ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}

To discuss:

  • The proposal recommended returning the string argument. We've not done that in the other helpers we added. It didn't make sense for the non-generic ThrowIfNull, but it could be done in the other helpers we've added new in this release. Folks ask for it in order to save a line of code, using the method as part of an expression.

Background and motivation

#69590 describes adding helper methods to ArgumentOutOfRangeException, which is terrific. But there are a few more common helper methods that we use our source base which would be generally useful.

API Proposal

namespace System.;

public class ArgumentException
{
    /// <summary>
    /// Throws either an <see cref="System.ArgumentNullException"/> or an <see cref="System.ArgumentException"/>
    /// if the specified string is <see langword="null"/> or whitespace respectively.
    /// </summary>
    /// <param name="argument">String to be checked for <see langword="null"/> or whitespace.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static string ThrowIfNullOrWhitespace([NotNull] string? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentNullException"/> if the string is <see langword="null"/>,
    /// or <see cref="System.ArgumentException"/> if it is empty.
    /// </summary>
    /// <param name="argument">String to be checked for <see langword="null"/> or empty.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentNullException"/> if the collection is <see langword="null"/>,
    /// or <see cref="System.ArgumentException"/> if it is empty.
    /// </summary>
    /// <param name="argument">The collection to evaluate.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <typeparam name="T">The type of objects in the collection.</typeparam>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static IEnumerable<T> ThrowIfNullOrEmpty<T>([NotNull] IEnumerable<T>? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentException"/> if the argument's buffer size is less than the required buffer size.
    /// </summary>
    /// <param name="bufferSize">The actual buffer size.</param>
    /// <param name="requiredSize">The required buffer size.</param>
    /// <param name="paramName">The name of the parameter to be checked.</param>
    public static void ThrowIfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "");
}

API Usage

ArgumentException.ThrowIfBufferTooSmall(destination.Length, source.Length);

Alternative Designs

No response

Risks

No response

@geeknoid geeknoid added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Nov 1, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Nov 1, 2022
@hrrrrustic
Copy link
Contributor

ThrowIfNullOrEmpty #62628
ThrowIfNullOrWhitespace #76529
IEnumerable ThrowIfNullOrEmpty #62628 (comment)

@stephentoub stephentoub modified the milestones: 7.0.x, 8.0.0 Nov 1, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 1, 2022
@ghost
Copy link

ghost commented Nov 1, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

#69590 describes adding helper methods to ArgumentOutOfRangeException, which is terrific. But there are a few more common helper methods that we use our source base which would be generally useful.

API Proposal

namespace System.;

public class ArgumentException
{
    /// <summary>
    /// Throws either an <see cref="System.ArgumentNullException"/> or an <see cref="System.ArgumentException"/>
    /// if the specified string is <see langword="null"/> or whitespace respectively.
    /// </summary>
    /// <param name="argument">String to be checked for <see langword="null"/> or whitespace.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static string ThrowIfNullOrWhitespace([NotNull] string? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentNullException"/> if the string is <see langword="null"/>,
    /// or <see cref="System.ArgumentException"/> if it is empty.
    /// </summary>
    /// <param name="argument">String to be checked for <see langword="null"/> or empty.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentNullException"/> if the collection is <see langword="null"/>,
    /// or <see cref="System.ArgumentException"/> if it is empty.
    /// </summary>
    /// <param name="argument">The collection to evaluate.</param>
    /// <param name="paramName">The name of the parameter being checked.</param>
    /// <typeparam name="T">The type of objects in the collection.</typeparam>
    /// <returns>The original value of <paramref name="argument"/>.</returns>
    [return: NotNull]
    public static IEnumerable<T> ThrowIfNullOrEmpty<T>([NotNull] IEnumerable<T>? argument, [CallerArgumentExpression("argument")] string paramName = "");

    /// <summary>
    /// Throws an <see cref="System.ArgumentException"/> if the argument's buffer size is less than the required buffer size.
    /// </summary>
    /// <param name="bufferSize">The actual buffer size.</param>
    /// <param name="requiredSize">The required buffer size.</param>
    /// <param name="paramName">The name of the parameter to be checked.</param>
    public static void ThrowIfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "");
}

API Usage

ArgumentException.ThrowIfBufferTooSmall(destination.Length, source.Length);

Alternative Designs

No response

Risks

No response

Author: geeknoid
Assignees: -
Labels:

api-suggestion, area-System.Runtime

Milestone: 8.0.0

@hrrrrustic
Copy link
Contributor

By the way, #69590 is waiting only @stephentoub final thoughts
#69590 (comment)

@teo-tsirpanis
Copy link
Contributor

  • The methods should return void.
  • The generic method should accept IReadOnlyCollection<T> to avoid enumerating the IEnumerable<T>.
    • And should we add an overload that takes ReadOnlySpan<T>?

@dakersnar dakersnar added the needs-author-action An issue or pull request that requires more info or actions from the author. label Nov 30, 2022
@ghost
Copy link

ghost commented Nov 30, 2022

This issue has been marked needs-author-action and may be missing some important information.

@stephentoub
Copy link
Member

stephentoub commented Dec 1, 2022

ThrowIfNullOrEmpty(string)

This one already exists.

ThrowIfNullOrWhitespace

We've discussed this one multiple times in the past and had opted not to add it each time. We could reconsider if it's more commonly needed than we'd found at the time.

ThrowIfNullOrEmpty(IEnumerable? argument)

This seems like a pit of failure from a performance perspective. Developers will change code like:

public void M(List<T> list)
{
    if (list is null) throw new ArgumentNullException(...);
    if (list.Count == 0) throw new ArgumentException(...);
    ...
}

and replace it with:

public void M(List<T> list)
{
    ArgumentException.ThrowIfNullOrEmpty(list);
    ...
}

and in doing so they just made their code more expensive, as the Count check would have been inlined as a simple field access, and now the code would need to do type tests, interface dispatch, etc. And then worse if the input wasn't an actual list, it's guiding developers to a pattern that leads to extra allocation from GetEnumerator.

If the 99% case we care about is for arrays, we could consider a T[]-based overload. A span-based overload would seem odd given spans can't be null. An I{ReadOnly}Collection-based overload veers into the pit of failure territory IMHO, and then the IEnumerable-based overload falls off that cliff.

ThrowIfBufferTooSmall(int bufferSize, int requiredSize, string paramName = "");

Maybe? I'd be interested in seeing what that would look like around dotnet/runtime, how many places we'd actually end up using it, etc.

@linkdotnet
Copy link

Just to raise my voice, I am very much in favor of adding ThrowIfNullOrWhitespace (see #76529).
Where this might not be very common in the runtime itself, for sure your everyday business logic might need this.

Yes, you can come around with Trim, but this will introduce new allocations for no good reason.

@dakersnar dakersnar removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Dec 1, 2022
@julealgon
Copy link

It would be incredibly nice if there was a way to define custom guard methods that would work on the same exception types. Say:

ArgumentException.ThrowIfMyCrazyScenarioMatchesHere(value);

Would it be possible to push for extension methods to static classes to allow that? Otherwise, guard methods become all inconsistent: some are found in native exception classes, and some are part of a different type. This hurts discoverability and consistency a lot, or forces people to wrap the existing methods in new classes to ensure consistency (creating a lot of duplication).

I understand this proposal would probably allow for this:

However it will be incredibly unlikely to move forward as a single change where everything becomes extendable. Having just static extensions could be done with not as much effort, and would still be very useful in a lot of situations. Support for other types of extensions could follow later as they are independent from one another.

@stefandv7
Copy link

I would also use ThrowIfNullOrWhitespace if it were available.

@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 1, 2023
@geeknoid
Copy link
Member Author

geeknoid commented May 4, 2023

What's the rationale for not wanting IfBufferTooSmall? I have an internal code base that can demonstrate an equivalent in action if examples are needed.

About returning the input as a return value, again this is something we use throughout our code base. Could this be added to the other exception utility functions retroactively or is changing a void return considered a breaking change?

@stephentoub
Copy link
Member

is changing a void return considered a breaking change?

It's a binary breaking change. The return type is part of the method identity; existing callers would start getting MissingMethodExceptions.

What's the rationale for not wanting IfBufferTooSmall?

From above:
"Maybe? I'd be interested in seeing what that would look like around dotnet/runtime, how many places we'd actually end up using it, etc."

@stephentoub stephentoub self-assigned this May 4, 2023
@bartonjs
Copy link
Member

bartonjs commented May 9, 2023

Video

  • We discussed having the return type be string vs void. We felt that this should be the same as the rest, so void, but the question of changing them all (except the ones that already shipped) to returning the argument can be raised at a later date.
public partial class ArgumentException
{
   public static void ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 9, 2023
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label May 9, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label May 10, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Jun 10, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants