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

Add Enum.TryParse overloads that allow restricting what input is considered valid #29167

Open
ericwj opened this issue Apr 4, 2019 · 4 comments
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime
Milestone

Comments

@ericwj
Copy link

ericwj commented Apr 4, 2019

It would be useful to have overloads of Enum.TryParse with an argument that allows specifying what kind of inputs are valid.

This would help the following primary use cases:

  1. To get false for all numeric strings. This is useful when the numeric value of the type underlying the enumeration is meaningless. E.g. the value "2" would parse to System.PlatformID.Win32NT but almost nobody cares and my guess is enumerations are quite commonly used to parse some kinds of invariant input.
  2. To get false for undefined numeric strings, but true for numeric strings that correspond to (a combination of) explicitly defined enumeration members. E.g. the value "16" happily parses to (System.ConsoleColor)16 today but again almost nobody cares and this is almost always an unintended side effect of the underlying type being numeric.

Secondary use cases would be:

  1. To add NumberStyles and IFormatProvider so the number parsing can be generalized by the user. Today, for example, Enum.TryParse simply does not take hexadecimal numbers, although it does take leading and trailing whitespace.
  2. To parse exactly one enumeration value, regardless of whether the enumeration type has the FlagsAttribute applied to it or not. If the enumeration defines names for the values 1, 2 and 4 and does not have a FlagsAttribute, this is in fact the same as 2. above. If it has a FlagsAttribute, then the value 3 would in the case of ConsoleModifiers correspond to Alt | Shift but in this mode Tryparse would still return false because the result is two members OR'ed together.

Having a way in the BCL to specify what types of input are considered valid also helps performance, since to parse first and then to check whether the value is in fact a (combination of) explicitly defined enumeration members involves duplicating the lookup of enumeration members, either in the framework or in user code. See e.g. #28841.

There is closely related work currently underway as part of #20008.

The way to implement this would probably be to introduce overload(s) for Enum.TryParse and Enum.TryParse<T> either:

a) with at least one extra boolean, but that would not be enough to cover the cases mentioned.
b) to introduce an enumeration similar to Enums.NET's EnumFormat albeit vastly simplified to strip all members that have to do with number formatting and all members having to do with alternative names in attributes or descriptions.

If this is something that is considered useful, I am happy to help specifying or building this on top of #20008.

@Gnbrkm41
Copy link
Contributor

Gnbrkm41 commented Apr 5, 2019

Definitely agree on this. Had some headaches debugging programs because of this. 😬 Ultimately I settled onto a combination of int.TryParse and Enum.TryParse. It definitely would be helpful if there's an overload for this.

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the 5.0 milestone Feb 1, 2020
@maryamariyan maryamariyan added the untriaged New issue has not been triaged by the area owner label Feb 23, 2020
@joperezr joperezr added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed untriaged New issue has not been triaged by the area owner labels Jul 7, 2020
@joperezr
Copy link
Member

joperezr commented Jul 7, 2020

Thanks for the proposal, in order to consider this we would need a formal API Proposal so that we can take this up for review. You can find more info on how to author one here https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md

@joperezr joperezr modified the milestones: 5.0.0, Future Jul 7, 2020
@ericwj
Copy link
Author

ericwj commented Jul 8, 2020

Background and Motivation

Make enums easier parseable with specific restrictions or relaxations as required by the developer.

Provides a solution to a series of issues such as #28841.

Provides various sources of issues with enums ways to implement solutions to these problems wholly or partially upon the proposed API's, #36502 (Microsoft.Extensions.Configuration), #35900 (System.Text.Json) and others.

Proposed API

API signature diff that is being proposed:

namespace System
{
+   [Flags]
+   public enum EnumStyles
+   {
+       None,
+       IgnoreCase = 1,
+       Defined = 2,
+       Single = 4,
+       Names = 8,
+   }
    public class Enum
    {
+       public static object Parse(Type enumType, string value, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null);
+       public static object Parse(Type enumType, ReadOnlySpan<char> value, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null);
+       public static bool TryParse(Type enumType, string value, out object result, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null);
+       public static bool TryParse(Type enumType, ReadOnlySpan<char> value, out object result, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null);
+       public static T Parse<T>(string value, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null) where T : struct, Enum;
+       public static T Parse<T>(ReadOnlySpan<char> value, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null) where T : struct, Enum;
+       public static bool TryParse<T>(string value, out T result, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null) where T : struct, Enum;
+       public static bool TryParse<T>(ReadOnlySpan<char> value, out T result, EnumStyles enumStyles = default, NumberStyles numberStyles = NumberStyles.Integer, IFormatProvider provider = null) where T : struct, Enum;
    }
}

The EnumStyles enumeration is named after NumberStyles, DateTimeStyles and TimeSpanStyles,
fulfilling a similar role.

The members of this enumeration named to be logical if you read e.g. TryParse...Names and
then shortening them for as much as I dare, keeping IgnoreCase as-is to match the existing
TryParse(Type, string, bool, out object) overload argument name ignoreCase.

The names are also chosen to be default when default in the sense that they do the same thing
as existing Parse and TryParse overloads do today. Alternatives include logical opposites
MatchCase, Undefined, Multiple and Numbers. Adding verbs like Require or Allow quickly
becomes unwieldy long if there needs to be multiple flags provided.

Naturally I build upon #20008 and its spinoffs to provide overloads that take ReadOnlySpan<char>.

A possible added convenience would be to add members to EnumStyles that provide names
for commonly used combinations of its flags; the below code would for example benefit from having
EnumStyles.DefinedSingleNames available.

Usage Examples

Instead of hand-wrought extension methods and whatnot that do Enum.TryParse
followed by int/long/byte.TryParse just to shoot down integral numbers,
a single call will suffice:

if (!TryParse("DarkGreen", out ConsoleColor result, 0
    | EnumStyles.Defined
    | EnumStyles.Single
    | EnumStyles.Names
    | EnumStyles.IgnoreCase))
    Console.WriteLine("Bark");

Since EnumStyles.Names is specified, neither the numberStyles nor the provider arguments
will be useful in this call, since these are meant to parse integral values more flexibly than
existing TryParse overloads through delegation to the appropriate int/long.TryParse methods
which take these arguments in their more complex forms.

Alternative Designs

One alternative design is to get rid of EnumStyles and provide several overloads instead, leaving some
of the remaining checks to the developer through the use of recently added API's such as Enum.IsDefined.

Risks

The most complex part of this addition to .NET would be enforcing EnumStyles.Single properly, both for
[Flags] attributes and regular ones. Not so much if names are being parsed, because these can,
after parsing them, individually be checked against Enum.IsDefined, but for numbers this function
returns false if the number is a combination of flags which the proposed API's must not do
if the EnumStyles.Single flag is not provided.

So if the enum being parsed is:

[Flags] enum E { None, A, B, C = 4 }

And TryParse is called with default(EnumStyles), 7 is valid input and will yield true as it does
for Enum.TryParse<E>(string, out E) today. However the value 7 would have to result in false
even if result returns A | B | C if EnumStyles.Single is provided, because it is a flags combination,
which would require trying to bin-fit 7 to possibly up to 63 unique enumeration flags not counting
those that map to the same numerical value, many could also not be powers of two
and with the possiblity that a whole range of bits isn't defined as (part of) any enumeration member at all.

@RenderMichael
Copy link
Contributor

This would help a number of problems I’ve personally run into.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Runtime
Projects
None yet
Development

No branches or pull requests

7 participants