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.ThrowIf method #99035

Closed
DrkWzrd opened this issue Feb 28, 2024 · 6 comments
Closed

[API Proposal]: ArgumentException.ThrowIf method #99035

DrkWzrd opened this issue Feb 28, 2024 · 6 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime

Comments

@DrkWzrd
Copy link

DrkWzrd commented Feb 28, 2024

Background and motivation

We are seeing the creation of many helper methods to throw argument exceptions in a clean, one-liner way. But I feel it lacks one core method. The ability to throw argument exception by some condition.

Check the difference in readability and "bloat"

//current
public ExampleConstructor(int integer, string? str, char chr, Something? something, IService? service)
{
    ArgumentOutOfRangeException.ThrowIfGreaterThan(integer, int.MaxValue / 2);
    ArgumentException.ThrowIfNullOrWhiteSpace(str);
    if (str.Equals("bad"))
    {
        throw new ArgumentException("This is a bad string", nameof(str));
    }
    if(char.IsLetter(chr))
    {
        throw new ArgumentException("Can not be a letter", nameof(chr));
    }
    ArgumentNullException.ThrowIfNull(something);
    if (!something.CheckState)
    {
        throw new ArgumentException("Something, invalid state", nameof(something));
    }
    ArgumentNullException.ThrowIfNull(service);
    if (!service.IsInitialized)
    {
        throw new ArgumentException("Service not initialized", nameof(service));
    }

    //Do things
}

//proposed
public ExampleConstructor(int integer, string? str, char chr, Something? something, IService? service)
{
    ArgumentOutOfRangeException.ThrowIfGreaterThan(integer, int.MaxValue / 2);

    ArgumentException.ThrowIfNullOrWhiteSpace(str);
    ArgumentException.ThrowIf(IsBadString, str, "This is a bad string");

    ArgumentException.ThrowIf(char.IsLetter, chr, "Can not be a letter");

    ArgumentNullException.ThrowIfNull(something);
    ArgumentException.ThrowIf(StateIsInvalid, something, "Something, invalid state");

    ArgumentNullException.ThrowIfNull(service);
    ArgumentException.ThrowIf(NotInitialized, service, "Service not initialized");
    
    //Do things
}

private /*static*/ bool IsBadString(string str) => str.Equals("bad");
private /*static*/ bool StateIsInvalid(Something smt) => !smt.CheckState;
private /*static*/ bool NotInitilized(IService svc) => !svc.IsInitialized;

And this can be used successfully to check things about IEnumerable (IsEmpty?, IsCountValid? AnyHasInvalidThings?, etc...)

API Proposal

namespace System.Collections.Generic;

public class ArgumentException
{
    public static void ThrowIf<T>(Func<T, bool> condition, T argument, string? message = null, [CallerArgumentExpression("argument")] string? paramName = null);
}

API Usage

The example above.

Alternative Designs

None

Risks

I don't see any, except another method to mantain (but is a tiny method ;) )

@DrkWzrd DrkWzrd added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 28, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Feb 28, 2024
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Feb 28, 2024
@DrkWzrd DrkWzrd changed the title [API Proposal]: [API Proposal]: ArgumentException.ThrowIf method Feb 28, 2024
@huoyaoyuan
Copy link
Member

The lambda is going to add significant overhead. Argument validation is typically expected to be more light-weighted.
The main benefits of the throw helper method are performance optimization and enabling CallerArgumentExpression.

If there's must-inline lambda like F#, this will be more acceptable.

@MichalPetryka
Copy link
Contributor

The lambda is going to add significant overhead.

It would probably not be the case with #85014 fixed and #85197 being revived.

@DrkWzrd
Copy link
Author

DrkWzrd commented Feb 28, 2024

The lambda is going to add significant overhead. Argument validation is typically expected to be more light-weighted. The main benefits of the throw helper method are performance optimization and enabling CallerArgumentExpression.

If there's must-inline lambda like F#, this will be more acceptable.

Sorry, I don't have that level of proficiency in .net, feel free to indicate me what performance-oriented changes should be done.

@colejohnson66
Copy link

colejohnson66 commented Feb 28, 2024

The proper method would just be passing a boolean straight in. There's no reason to pass an immediately invoked delegate if one can just pass the result. Sure, there's no logical difference (program will execute the same) between these two blocks of code:

void Test1(string str) {
    ArgumentException.ThrowIf(x => x != "expected", str);
}

void Test2(string str) {
    ArgumentException.ThrowIf(str != "expected", str);
}

But the former (Test1) will allocate a new delegate on the heap, which will then have to be invoked. Eventually, the garbage collector will get involved as well. All that is overhead that we want to avoid.

The "correct" API would look like this:

namespace System.Collections.Generic;

public class ArgumentException
{
    public static void ThrowIf<T>(bool condition, T argument, string? message = null, [CallerArgumentExpression("argument")] string? paramName = null);
}

Tangent: if your use case is to validate an IEnumerable before actually using it, you should consider alternative methods. Multiple enumeration is a big no-no. Essentially:

// bad
void Test<T>(IEnumerable<T> source)
{
    ArgumentException.ThrowIf(!source.Any(), source);
 
    // this will cause multiple enumeration which incur performance issues or even exceptions
    foreach (T element in source)
        Use(element);
}

// good
void Test<T>(IEnumerable<T> source)
{
    using IEnumerator<T> e = source.GetEnumerator();
    if (!e.MoveNext())
        throw new InvalidOperationException("Source is empty.");

    do
    {
        Use(e.Current);
    } while (e.MoveNext());
}

@stephentoub
Copy link
Member

The "correct" API would look like this:

But even then, this requires producing the message before the condition is checked, and that itself frequently has overheads, eg reading a string from a resx, or string.Fornat'ing a message.

This has been discussed before in various forms. We have no plans to add such an overload. Just do if (!condition) throw new ArgumentException(...).

@vcsjones vcsjones added area-System.Runtime and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Feb 28, 2024
@ghost
Copy link

ghost commented Feb 28, 2024

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

We are seeing the creation of many helper methods to throw argument exceptions in a clean, one-liner way. But I feel it lacks one core method. The ability to throw argument exception by some condition.

Check the difference in readability and "bloat"

//current
public ExampleConstructor(int integer, string? str, char chr, Something? something, IService? service)
{
    ArgumentOutOfRangeException.ThrowIfGreaterThan(integer, int.MaxValue / 2);
    ArgumentException.ThrowIfNullOrWhiteSpace(str);
    if (str.Equals("bad"))
    {
        throw new ArgumentException("This is a bad string", nameof(str));
    }
    if(char.IsLetter(chr))
    {
        throw new ArgumentException("Can not be a letter", nameof(chr));
    }
    ArgumentNullException.ThrowIfNull(something);
    if (!something.CheckState)
    {
        throw new ArgumentException("Something, invalid state", nameof(something));
    }
    ArgumentNullException.ThrowIfNull(service);
    if (!service.IsInitialized)
    {
        throw new ArgumentException("Service not initialized", nameof(service));
    }

    //Do things
}

//proposed
public ExampleConstructor(int integer, string? str, char chr, Something? something, IService? service)
{
    ArgumentOutOfRangeException.ThrowIfGreaterThan(integer, int.MaxValue / 2);

    ArgumentException.ThrowIfNullOrWhiteSpace(str);
    ArgumentException.ThrowIf(IsBadString, str, "This is a bad string");

    ArgumentException.ThrowIf(char.IsLetter, chr, "Can not be a letter");

    ArgumentNullException.ThrowIfNull(something);
    ArgumentException.ThrowIf(StateIsInvalid, something, "Something, invalid state");

    ArgumentNullException.ThrowIfNull(service);
    ArgumentException.ThrowIf(NotInitialized, service, "Service not initialized");
    
    //Do things
}

private /*static*/ bool IsBadString(string str) => str.Equals("bad");
private /*static*/ bool StateIsInvalid(Something smt) => !smt.CheckState;
private /*static*/ bool NotInitilized(IService svc) => !svc.IsInitialized;

And this can be used successfully to check things about IEnumerable (IsEmpty?, IsCountValid? AnyHasInvalidThings?, etc...)

API Proposal

namespace System.Collections.Generic;

public class ArgumentException
{
    public static void ThrowIf<T>(Func<T, bool> condition, T argument, string? message = null, [CallerArgumentExpression("argument")] string? paramName = null);
}

API Usage

The example above.

Alternative Designs

None

Risks

I don't see any, except another method to mantain (but is a tiny method ;) )

Author: DrkWzrd
Assignees: -
Labels:

api-suggestion, area-System.Runtime, untriaged

Milestone: -

@stephentoub stephentoub closed this as not planned Won't fix, can't repro, duplicate, stale Apr 22, 2024
@dotnet-policy-service dotnet-policy-service bot removed the untriaged New issue has not been triaged by the area owner label Apr 22, 2024
@github-actions github-actions bot locked and limited conversation to collaborators May 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime
Projects
None yet
Development

No branches or pull requests

6 participants