General purpose replacement for enum with FlagsAttribute





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{
margin-bottom:0;
}








10












$begingroup$


Enums with the FlagsAttribute have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum is closed/final.



My alternative Option class should solve these two issues. It can be used either on its own or be inherited from. The two static Create factories take care of the Flag value for the specified Category. The Category groups options together. HasFlag is called here Contains. Other than this it also implements the usual set of operators and parsing.



[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();

private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();

public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}

private string DebuggerDisplay => ToString();

[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }

public SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }

public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}

[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}

private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}

public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";

#region IEquatable

public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option);

public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);

#endregion

public int CompareTo(Option other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static implicit operator Option(string value) => Parse(value);

public static implicit operator Option(int value) => FromValue(value);

#region Operators

public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);

public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;

public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);

#endregion

private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}


This should replace the previous enum




[Flags]
public enum FeatureOptions
{
None = 0,

/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}



with



[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}


that is based on a new FeatureOption type



public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}


They can be used exactly like classic enums:



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);

var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);

var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);

Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}




Questions




  • Is this as extendable as I think it is?

  • Are there any APIs missing that I didn't think of or would be convenient?

  • What do you think about the automatic Flag maintenance and options creation?










share|improve this question











$endgroup$














  • $begingroup$
    The current commit.
    $endgroup$
    – t3chb0t
    May 26 at 12:06










  • $begingroup$
    Comments are not for extended discussion; this conversation has been moved to chat.
    $endgroup$
    – rolfl
    May 27 at 13:15










  • $begingroup$
    wow, I got another downvote... how so?
    $endgroup$
    – t3chb0t
    Jul 12 at 11:50


















10












$begingroup$


Enums with the FlagsAttribute have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum is closed/final.



My alternative Option class should solve these two issues. It can be used either on its own or be inherited from. The two static Create factories take care of the Flag value for the specified Category. The Category groups options together. HasFlag is called here Contains. Other than this it also implements the usual set of operators and parsing.



[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();

private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();

public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}

private string DebuggerDisplay => ToString();

[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }

public SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }

public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}

[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}

private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}

public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";

#region IEquatable

public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option);

public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);

#endregion

public int CompareTo(Option other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static implicit operator Option(string value) => Parse(value);

public static implicit operator Option(int value) => FromValue(value);

#region Operators

public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);

public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;

public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);

#endregion

private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}


This should replace the previous enum




[Flags]
public enum FeatureOptions
{
None = 0,

/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}



with



[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}


that is based on a new FeatureOption type



public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}


They can be used exactly like classic enums:



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);

var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);

var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);

Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}




Questions




  • Is this as extendable as I think it is?

  • Are there any APIs missing that I didn't think of or would be convenient?

  • What do you think about the automatic Flag maintenance and options creation?










share|improve this question











$endgroup$














  • $begingroup$
    The current commit.
    $endgroup$
    – t3chb0t
    May 26 at 12:06










  • $begingroup$
    Comments are not for extended discussion; this conversation has been moved to chat.
    $endgroup$
    – rolfl
    May 27 at 13:15










  • $begingroup$
    wow, I got another downvote... how so?
    $endgroup$
    – t3chb0t
    Jul 12 at 11:50














10












10








10


1



$begingroup$


Enums with the FlagsAttribute have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum is closed/final.



My alternative Option class should solve these two issues. It can be used either on its own or be inherited from. The two static Create factories take care of the Flag value for the specified Category. The Category groups options together. HasFlag is called here Contains. Other than this it also implements the usual set of operators and parsing.



[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();

private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();

public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}

private string DebuggerDisplay => ToString();

[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }

public SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }

public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}

[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}

private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}

public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";

#region IEquatable

public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option);

public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);

#endregion

public int CompareTo(Option other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static implicit operator Option(string value) => Parse(value);

public static implicit operator Option(int value) => FromValue(value);

#region Operators

public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);

public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;

public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);

#endregion

private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}


This should replace the previous enum




[Flags]
public enum FeatureOptions
{
None = 0,

/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}



with



[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}


that is based on a new FeatureOption type



public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}


They can be used exactly like classic enums:



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);

var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);

var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);

Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}




Questions




  • Is this as extendable as I think it is?

  • Are there any APIs missing that I didn't think of or would be convenient?

  • What do you think about the automatic Flag maintenance and options creation?










share|improve this question











$endgroup$




Enums with the FlagsAttribute have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum is closed/final.



My alternative Option class should solve these two issues. It can be used either on its own or be inherited from. The two static Create factories take care of the Flag value for the specified Category. The Category groups options together. HasFlag is called here Contains. Other than this it also implements the usual set of operators and parsing.



[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();

private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();

public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}

private string DebuggerDisplay => ToString();

[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }

public SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }

public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}

[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}

private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");

return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}

public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";

#region IEquatable

public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option);

public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);

#endregion

public int CompareTo(Option other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static implicit operator Option(string value) => Parse(value);

public static implicit operator Option(int value) => FromValue(value);

#region Operators

public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);

public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;

public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);

#endregion

private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}


This should replace the previous enum




[Flags]
public enum FeatureOptions
{
None = 0,

/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}



with



[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}


that is based on a new FeatureOption type



public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}


They can be used exactly like classic enums:



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);

var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);

var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);

Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}




Questions




  • Is this as extendable as I think it is?

  • Are there any APIs missing that I didn't think of or would be convenient?

  • What do you think about the automatic Flag maintenance and options creation?







c# api inheritance enum






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Jun 16 at 16:07









dfhwze

13.1k3 gold badges22 silver badges92 bronze badges




13.1k3 gold badges22 silver badges92 bronze badges










asked May 26 at 12:02









t3chb0tt3chb0t

38.6k7 gold badges62 silver badges142 bronze badges




38.6k7 gold badges62 silver badges142 bronze badges















  • $begingroup$
    The current commit.
    $endgroup$
    – t3chb0t
    May 26 at 12:06










  • $begingroup$
    Comments are not for extended discussion; this conversation has been moved to chat.
    $endgroup$
    – rolfl
    May 27 at 13:15










  • $begingroup$
    wow, I got another downvote... how so?
    $endgroup$
    – t3chb0t
    Jul 12 at 11:50


















  • $begingroup$
    The current commit.
    $endgroup$
    – t3chb0t
    May 26 at 12:06










  • $begingroup$
    Comments are not for extended discussion; this conversation has been moved to chat.
    $endgroup$
    – rolfl
    May 27 at 13:15










  • $begingroup$
    wow, I got another downvote... how so?
    $endgroup$
    – t3chb0t
    Jul 12 at 11:50
















$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06




$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06












$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl
May 27 at 13:15




$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl
May 27 at 13:15












$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50




$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50










3 Answers
3






active

oldest

votes


















10














$begingroup$

int NextFlag(string category)



I'd expect this to throw when it runs out of flags.



I really really don't like that the first flag just happens to be 0: that depends on the order in which they are defined, and isn't written down anywhere.



Option FromValue(int value, params Option options)



I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?



The Aggregate seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()).



var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));


The flag accumulator could be its own method, shared with the Contains method, since it seems like a meaningful task in its own right.



Misc





  • It should check for name reuse: this bug should throw in your example:



    Telemetry = Option.Create<FeatureOption>(nameof(Warn));


  • Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with enum.


  • Option(string value) => Parse(value); looks broken, as does operator Option(int value) => FromValue(value), because they don't provide any options from which to select.


  • There are a few [NotNull]s strewn about the place, and some that appear to be missing (e.g. both Create methods presumably don't return null, nor should Parse; the parameters to the implicit operators).


  • The comparer will happily compare Options from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method taking params Option, and feed it in this instance also.


  • You could make use of [System.Runtime.CompilerServices.CallerMemberName] in Option.Create<T>, which could mitigate bugs like the misnaming of Telemetry.







share|improve this answer











$endgroup$











  • 1




    $begingroup$
    That's a lot to improve! I'd better get to work...
    $endgroup$
    – t3chb0t
    May 26 at 14:33










  • $begingroup$
    Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
    $endgroup$
    – t3chb0t
    May 27 at 9:24










  • $begingroup$
    @t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
    $endgroup$
    – dfhwze
    May 28 at 10:08










  • $begingroup$
    @dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
    $endgroup$
    – t3chb0t
    May 28 at 17:27












  • $begingroup$
    @t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
    $endgroup$
    – dfhwze
    May 28 at 17:30





















9














$begingroup$


Is this as extendable as I think it is?




Does it work for multi-bit flags? For instance



[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}



Are there any APIs missing that I didn't think of or would be
convinient?




BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.




What do you think about the automatic Flag maintenance and options
creation?




It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.






share|improve this answer











$endgroup$















  • $begingroup$
    It is very likely that I don't know what multibit flags are. Could give me an example?
    $endgroup$
    – t3chb0t
    May 26 at 12:47






  • 1




    $begingroup$
    @t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
    $endgroup$
    – dfhwze
    May 26 at 12:49



















4














$begingroup$

(self-answer)





v3



I wanted to use v2 of this code (below) to upgrade my old MimeType that was very similar but it turned out I cannot because I need string values (like application/json) and not numerical ones (like 1) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString and replaced binary operations with HashSets. Alternatively this could use a generic value but currently I don't see any use for them.



[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);

public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract IImmutableSet<SoftString> Values { get; }

public abstract bool IsFlag { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);

// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;

static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);

// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}

protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}

[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));

/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }

/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

public override IImmutableSet<SoftString> Values { get; }

/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;

#endregion

#region Factories

public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}

[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}

if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}

var newOption = New(name, values);

if (name == Unknown)
{
return newOption;
}

Known = Known.Add(newOption);
return newOption;
}

private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}

[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}

[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}

private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option) => this | option;

public T Reset(Option<T> option) => this ^ option;

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:asc}";

public bool Contains(T option) => Values.Overlaps(option.Values);

#region IEquatable

public bool Equals(Option<T> other) => Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}

#endregion
}




v2



I have made a couple of changes so here's the summary and the improved code:




  • Using CallerMemberName for automatic option names, however, it's still possible to create custom options with anyname.

  • Using generic Option<T> to remove the Dictionary and provide a few default properties such as None, All or Max, Bits.

  • Cleaned-up naming; now parsing APIs are called FromName and FromValue

  • Added internal set of options so that I can check whether an option is already defined and use it for other properties like All, Max and Bits.

  • Added multi-bit support.

  • Not using BitVector32 yet... maybe later.

  • Added IFormattable interface and three formats: names, flags and names+flags.

  • Encapsulated operators | and ^ respectively as Set and Reset.

  • Added Flags property that enumerates all bits of an option.


[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract int Flag { get; }

/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);

private static readonly OptionComparer Comparer = new OptionComparer();

private static IImmutableSet<T> Options;

static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}

protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");

Name = name;
Flag = flag;
}

#region Default options

[NotNull]
public static T None => Options.First();

[NotNull]
public static T Max => Options.Last();

[NotNull]
public static IEnumerable<T> All => Options;

#endregion

[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }

public override bool IsBit => (Flag & (Flag - 1)) == 0;

#endregion

[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);

#region Factories

[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}

var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);

return newOption;
}

[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}

private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}

public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}

[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));

return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}

// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}

var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}

private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option)
{
return this | option;
}

public T Reset(Option<T> option)
{
return this ^ option;
}

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:names}";

public bool Contains(T option) => Contains(option.Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

public int CompareTo(Option<T> other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

#region IEquatable

public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;

public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;

public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;

[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);

[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);

private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}

#endregion

private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}


A new option-set can now be defined by deriving it from Option<T> and adding static properties for the desired flags:



public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();

public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}


Since there is only one option-class now, tests have also become simpler.



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);

var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);

var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);

var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";

Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);

Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}




The intended usage is:




  • Logger layer where the user can define their custom log-levels

  • FeatureService where the user can define their custom behaviours

  • Other services that work with some default options and let the user customize it with their domain-specific flags.






share|improve this answer











$endgroup$















  • $begingroup$
    Does a private protected constructor work? I have never seen this before.
    $endgroup$
    – dfhwze
    Jun 16 at 16:03






  • 1




    $begingroup$
    @dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
    $endgroup$
    – t3chb0t
    Jun 16 at 16:07













Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/4.0/"u003ecc by-sa 4.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});















draft saved

draft discarded
















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221057%2fgeneral-purpose-replacement-for-enum-with-flagsattribute%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























3 Answers
3






active

oldest

votes








3 Answers
3






active

oldest

votes









active

oldest

votes






active

oldest

votes









10














$begingroup$

int NextFlag(string category)



I'd expect this to throw when it runs out of flags.



I really really don't like that the first flag just happens to be 0: that depends on the order in which they are defined, and isn't written down anywhere.



Option FromValue(int value, params Option options)



I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?



The Aggregate seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()).



var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));


The flag accumulator could be its own method, shared with the Contains method, since it seems like a meaningful task in its own right.



Misc





  • It should check for name reuse: this bug should throw in your example:



    Telemetry = Option.Create<FeatureOption>(nameof(Warn));


  • Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with enum.


  • Option(string value) => Parse(value); looks broken, as does operator Option(int value) => FromValue(value), because they don't provide any options from which to select.


  • There are a few [NotNull]s strewn about the place, and some that appear to be missing (e.g. both Create methods presumably don't return null, nor should Parse; the parameters to the implicit operators).


  • The comparer will happily compare Options from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method taking params Option, and feed it in this instance also.


  • You could make use of [System.Runtime.CompilerServices.CallerMemberName] in Option.Create<T>, which could mitigate bugs like the misnaming of Telemetry.







share|improve this answer











$endgroup$











  • 1




    $begingroup$
    That's a lot to improve! I'd better get to work...
    $endgroup$
    – t3chb0t
    May 26 at 14:33










  • $begingroup$
    Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
    $endgroup$
    – t3chb0t
    May 27 at 9:24










  • $begingroup$
    @t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
    $endgroup$
    – dfhwze
    May 28 at 10:08










  • $begingroup$
    @dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
    $endgroup$
    – t3chb0t
    May 28 at 17:27












  • $begingroup$
    @t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
    $endgroup$
    – dfhwze
    May 28 at 17:30


















10














$begingroup$

int NextFlag(string category)



I'd expect this to throw when it runs out of flags.



I really really don't like that the first flag just happens to be 0: that depends on the order in which they are defined, and isn't written down anywhere.



Option FromValue(int value, params Option options)



I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?



The Aggregate seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()).



var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));


The flag accumulator could be its own method, shared with the Contains method, since it seems like a meaningful task in its own right.



Misc





  • It should check for name reuse: this bug should throw in your example:



    Telemetry = Option.Create<FeatureOption>(nameof(Warn));


  • Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with enum.


  • Option(string value) => Parse(value); looks broken, as does operator Option(int value) => FromValue(value), because they don't provide any options from which to select.


  • There are a few [NotNull]s strewn about the place, and some that appear to be missing (e.g. both Create methods presumably don't return null, nor should Parse; the parameters to the implicit operators).


  • The comparer will happily compare Options from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method taking params Option, and feed it in this instance also.


  • You could make use of [System.Runtime.CompilerServices.CallerMemberName] in Option.Create<T>, which could mitigate bugs like the misnaming of Telemetry.







share|improve this answer











$endgroup$











  • 1




    $begingroup$
    That's a lot to improve! I'd better get to work...
    $endgroup$
    – t3chb0t
    May 26 at 14:33










  • $begingroup$
    Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
    $endgroup$
    – t3chb0t
    May 27 at 9:24










  • $begingroup$
    @t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
    $endgroup$
    – dfhwze
    May 28 at 10:08










  • $begingroup$
    @dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
    $endgroup$
    – t3chb0t
    May 28 at 17:27












  • $begingroup$
    @t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
    $endgroup$
    – dfhwze
    May 28 at 17:30
















10














10










10







$begingroup$

int NextFlag(string category)



I'd expect this to throw when it runs out of flags.



I really really don't like that the first flag just happens to be 0: that depends on the order in which they are defined, and isn't written down anywhere.



Option FromValue(int value, params Option options)



I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?



The Aggregate seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()).



var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));


The flag accumulator could be its own method, shared with the Contains method, since it seems like a meaningful task in its own right.



Misc





  • It should check for name reuse: this bug should throw in your example:



    Telemetry = Option.Create<FeatureOption>(nameof(Warn));


  • Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with enum.


  • Option(string value) => Parse(value); looks broken, as does operator Option(int value) => FromValue(value), because they don't provide any options from which to select.


  • There are a few [NotNull]s strewn about the place, and some that appear to be missing (e.g. both Create methods presumably don't return null, nor should Parse; the parameters to the implicit operators).


  • The comparer will happily compare Options from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method taking params Option, and feed it in this instance also.


  • You could make use of [System.Runtime.CompilerServices.CallerMemberName] in Option.Create<T>, which could mitigate bugs like the misnaming of Telemetry.







share|improve this answer











$endgroup$



int NextFlag(string category)



I'd expect this to throw when it runs out of flags.



I really really don't like that the first flag just happens to be 0: that depends on the order in which they are defined, and isn't written down anywhere.



Option FromValue(int value, params Option options)



I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?



The Aggregate seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()).



var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));


The flag accumulator could be its own method, shared with the Contains method, since it seems like a meaningful task in its own right.



Misc





  • It should check for name reuse: this bug should throw in your example:



    Telemetry = Option.Create<FeatureOption>(nameof(Warn));


  • Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with enum.


  • Option(string value) => Parse(value); looks broken, as does operator Option(int value) => FromValue(value), because they don't provide any options from which to select.


  • There are a few [NotNull]s strewn about the place, and some that appear to be missing (e.g. both Create methods presumably don't return null, nor should Parse; the parameters to the implicit operators).


  • The comparer will happily compare Options from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method taking params Option, and feed it in this instance also.


  • You could make use of [System.Runtime.CompilerServices.CallerMemberName] in Option.Create<T>, which could mitigate bugs like the misnaming of Telemetry.








share|improve this answer














share|improve this answer



share|improve this answer








edited May 26 at 13:29

























answered May 26 at 13:04









VisualMelonVisualMelon

6,71215 silver badges44 bronze badges




6,71215 silver badges44 bronze badges











  • 1




    $begingroup$
    That's a lot to improve! I'd better get to work...
    $endgroup$
    – t3chb0t
    May 26 at 14:33










  • $begingroup$
    Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
    $endgroup$
    – t3chb0t
    May 27 at 9:24










  • $begingroup$
    @t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
    $endgroup$
    – dfhwze
    May 28 at 10:08










  • $begingroup$
    @dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
    $endgroup$
    – t3chb0t
    May 28 at 17:27












  • $begingroup$
    @t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
    $endgroup$
    – dfhwze
    May 28 at 17:30
















  • 1




    $begingroup$
    That's a lot to improve! I'd better get to work...
    $endgroup$
    – t3chb0t
    May 26 at 14:33










  • $begingroup$
    Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
    $endgroup$
    – t3chb0t
    May 27 at 9:24










  • $begingroup$
    @t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
    $endgroup$
    – dfhwze
    May 28 at 10:08










  • $begingroup$
    @dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
    $endgroup$
    – t3chb0t
    May 28 at 17:27












  • $begingroup$
    @t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
    $endgroup$
    – dfhwze
    May 28 at 17:30










1




1




$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33




$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33












$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24




$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24












$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08




$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08












$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27






$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with FromValue where I look for known options, otherwise I return Unknown, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27














$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30






$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30















9














$begingroup$


Is this as extendable as I think it is?




Does it work for multi-bit flags? For instance



[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}



Are there any APIs missing that I didn't think of or would be
convinient?




BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.




What do you think about the automatic Flag maintenance and options
creation?




It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.






share|improve this answer











$endgroup$















  • $begingroup$
    It is very likely that I don't know what multibit flags are. Could give me an example?
    $endgroup$
    – t3chb0t
    May 26 at 12:47






  • 1




    $begingroup$
    @t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
    $endgroup$
    – dfhwze
    May 26 at 12:49
















9














$begingroup$


Is this as extendable as I think it is?




Does it work for multi-bit flags? For instance



[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}



Are there any APIs missing that I didn't think of or would be
convinient?




BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.




What do you think about the automatic Flag maintenance and options
creation?




It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.






share|improve this answer











$endgroup$















  • $begingroup$
    It is very likely that I don't know what multibit flags are. Could give me an example?
    $endgroup$
    – t3chb0t
    May 26 at 12:47






  • 1




    $begingroup$
    @t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
    $endgroup$
    – dfhwze
    May 26 at 12:49














9














9










9







$begingroup$


Is this as extendable as I think it is?




Does it work for multi-bit flags? For instance



[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}



Are there any APIs missing that I didn't think of or would be
convinient?




BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.




What do you think about the automatic Flag maintenance and options
creation?




It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.






share|improve this answer











$endgroup$




Is this as extendable as I think it is?




Does it work for multi-bit flags? For instance



[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}



Are there any APIs missing that I didn't think of or would be
convinient?




BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.




What do you think about the automatic Flag maintenance and options
creation?




It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.







share|improve this answer














share|improve this answer



share|improve this answer








edited May 26 at 12:58

























answered May 26 at 12:44









dfhwzedfhwze

13.1k3 gold badges22 silver badges92 bronze badges




13.1k3 gold badges22 silver badges92 bronze badges















  • $begingroup$
    It is very likely that I don't know what multibit flags are. Could give me an example?
    $endgroup$
    – t3chb0t
    May 26 at 12:47






  • 1




    $begingroup$
    @t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
    $endgroup$
    – dfhwze
    May 26 at 12:49


















  • $begingroup$
    It is very likely that I don't know what multibit flags are. Could give me an example?
    $endgroup$
    – t3chb0t
    May 26 at 12:47






  • 1




    $begingroup$
    @t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
    $endgroup$
    – dfhwze
    May 26 at 12:49
















$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47




$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47




1




1




$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49




$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49











4














$begingroup$

(self-answer)





v3



I wanted to use v2 of this code (below) to upgrade my old MimeType that was very similar but it turned out I cannot because I need string values (like application/json) and not numerical ones (like 1) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString and replaced binary operations with HashSets. Alternatively this could use a generic value but currently I don't see any use for them.



[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);

public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract IImmutableSet<SoftString> Values { get; }

public abstract bool IsFlag { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);

// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;

static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);

// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}

protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}

[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));

/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }

/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

public override IImmutableSet<SoftString> Values { get; }

/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;

#endregion

#region Factories

public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}

[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}

if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}

var newOption = New(name, values);

if (name == Unknown)
{
return newOption;
}

Known = Known.Add(newOption);
return newOption;
}

private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}

[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}

[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}

private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option) => this | option;

public T Reset(Option<T> option) => this ^ option;

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:asc}";

public bool Contains(T option) => Values.Overlaps(option.Values);

#region IEquatable

public bool Equals(Option<T> other) => Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}

#endregion
}




v2



I have made a couple of changes so here's the summary and the improved code:




  • Using CallerMemberName for automatic option names, however, it's still possible to create custom options with anyname.

  • Using generic Option<T> to remove the Dictionary and provide a few default properties such as None, All or Max, Bits.

  • Cleaned-up naming; now parsing APIs are called FromName and FromValue

  • Added internal set of options so that I can check whether an option is already defined and use it for other properties like All, Max and Bits.

  • Added multi-bit support.

  • Not using BitVector32 yet... maybe later.

  • Added IFormattable interface and three formats: names, flags and names+flags.

  • Encapsulated operators | and ^ respectively as Set and Reset.

  • Added Flags property that enumerates all bits of an option.


[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract int Flag { get; }

/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);

private static readonly OptionComparer Comparer = new OptionComparer();

private static IImmutableSet<T> Options;

static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}

protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");

Name = name;
Flag = flag;
}

#region Default options

[NotNull]
public static T None => Options.First();

[NotNull]
public static T Max => Options.Last();

[NotNull]
public static IEnumerable<T> All => Options;

#endregion

[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }

public override bool IsBit => (Flag & (Flag - 1)) == 0;

#endregion

[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);

#region Factories

[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}

var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);

return newOption;
}

[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}

private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}

public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}

[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));

return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}

// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}

var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}

private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option)
{
return this | option;
}

public T Reset(Option<T> option)
{
return this ^ option;
}

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:names}";

public bool Contains(T option) => Contains(option.Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

public int CompareTo(Option<T> other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

#region IEquatable

public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;

public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;

public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;

[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);

[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);

private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}

#endregion

private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}


A new option-set can now be defined by deriving it from Option<T> and adding static properties for the desired flags:



public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();

public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}


Since there is only one option-class now, tests have also become simpler.



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);

var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);

var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);

var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";

Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);

Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}




The intended usage is:




  • Logger layer where the user can define their custom log-levels

  • FeatureService where the user can define their custom behaviours

  • Other services that work with some default options and let the user customize it with their domain-specific flags.






share|improve this answer











$endgroup$















  • $begingroup$
    Does a private protected constructor work? I have never seen this before.
    $endgroup$
    – dfhwze
    Jun 16 at 16:03






  • 1




    $begingroup$
    @dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
    $endgroup$
    – t3chb0t
    Jun 16 at 16:07
















4














$begingroup$

(self-answer)





v3



I wanted to use v2 of this code (below) to upgrade my old MimeType that was very similar but it turned out I cannot because I need string values (like application/json) and not numerical ones (like 1) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString and replaced binary operations with HashSets. Alternatively this could use a generic value but currently I don't see any use for them.



[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);

public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract IImmutableSet<SoftString> Values { get; }

public abstract bool IsFlag { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);

// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;

static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);

// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}

protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}

[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));

/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }

/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

public override IImmutableSet<SoftString> Values { get; }

/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;

#endregion

#region Factories

public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}

[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}

if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}

var newOption = New(name, values);

if (name == Unknown)
{
return newOption;
}

Known = Known.Add(newOption);
return newOption;
}

private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}

[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}

[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}

private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option) => this | option;

public T Reset(Option<T> option) => this ^ option;

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:asc}";

public bool Contains(T option) => Values.Overlaps(option.Values);

#region IEquatable

public bool Equals(Option<T> other) => Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}

#endregion
}




v2



I have made a couple of changes so here's the summary and the improved code:




  • Using CallerMemberName for automatic option names, however, it's still possible to create custom options with anyname.

  • Using generic Option<T> to remove the Dictionary and provide a few default properties such as None, All or Max, Bits.

  • Cleaned-up naming; now parsing APIs are called FromName and FromValue

  • Added internal set of options so that I can check whether an option is already defined and use it for other properties like All, Max and Bits.

  • Added multi-bit support.

  • Not using BitVector32 yet... maybe later.

  • Added IFormattable interface and three formats: names, flags and names+flags.

  • Encapsulated operators | and ^ respectively as Set and Reset.

  • Added Flags property that enumerates all bits of an option.


[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract int Flag { get; }

/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);

private static readonly OptionComparer Comparer = new OptionComparer();

private static IImmutableSet<T> Options;

static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}

protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");

Name = name;
Flag = flag;
}

#region Default options

[NotNull]
public static T None => Options.First();

[NotNull]
public static T Max => Options.Last();

[NotNull]
public static IEnumerable<T> All => Options;

#endregion

[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }

public override bool IsBit => (Flag & (Flag - 1)) == 0;

#endregion

[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);

#region Factories

[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}

var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);

return newOption;
}

[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}

private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}

public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}

[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));

return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}

// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}

var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}

private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option)
{
return this | option;
}

public T Reset(Option<T> option)
{
return this ^ option;
}

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:names}";

public bool Contains(T option) => Contains(option.Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

public int CompareTo(Option<T> other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

#region IEquatable

public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;

public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;

public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;

[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);

[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);

private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}

#endregion

private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}


A new option-set can now be defined by deriving it from Option<T> and adding static properties for the desired flags:



public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();

public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}


Since there is only one option-class now, tests have also become simpler.



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);

var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);

var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);

var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";

Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);

Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}




The intended usage is:




  • Logger layer where the user can define their custom log-levels

  • FeatureService where the user can define their custom behaviours

  • Other services that work with some default options and let the user customize it with their domain-specific flags.






share|improve this answer











$endgroup$















  • $begingroup$
    Does a private protected constructor work? I have never seen this before.
    $endgroup$
    – dfhwze
    Jun 16 at 16:03






  • 1




    $begingroup$
    @dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
    $endgroup$
    – t3chb0t
    Jun 16 at 16:07














4














4










4







$begingroup$

(self-answer)





v3



I wanted to use v2 of this code (below) to upgrade my old MimeType that was very similar but it turned out I cannot because I need string values (like application/json) and not numerical ones (like 1) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString and replaced binary operations with HashSets. Alternatively this could use a generic value but currently I don't see any use for them.



[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);

public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract IImmutableSet<SoftString> Values { get; }

public abstract bool IsFlag { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);

// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;

static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);

// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}

protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}

[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));

/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }

/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

public override IImmutableSet<SoftString> Values { get; }

/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;

#endregion

#region Factories

public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}

[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}

if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}

var newOption = New(name, values);

if (name == Unknown)
{
return newOption;
}

Known = Known.Add(newOption);
return newOption;
}

private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}

[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}

[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}

private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option) => this | option;

public T Reset(Option<T> option) => this ^ option;

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:asc}";

public bool Contains(T option) => Values.Overlaps(option.Values);

#region IEquatable

public bool Equals(Option<T> other) => Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}

#endregion
}




v2



I have made a couple of changes so here's the summary and the improved code:




  • Using CallerMemberName for automatic option names, however, it's still possible to create custom options with anyname.

  • Using generic Option<T> to remove the Dictionary and provide a few default properties such as None, All or Max, Bits.

  • Cleaned-up naming; now parsing APIs are called FromName and FromValue

  • Added internal set of options so that I can check whether an option is already defined and use it for other properties like All, Max and Bits.

  • Added multi-bit support.

  • Not using BitVector32 yet... maybe later.

  • Added IFormattable interface and three formats: names, flags and names+flags.

  • Encapsulated operators | and ^ respectively as Set and Reset.

  • Added Flags property that enumerates all bits of an option.


[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract int Flag { get; }

/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);

private static readonly OptionComparer Comparer = new OptionComparer();

private static IImmutableSet<T> Options;

static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}

protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");

Name = name;
Flag = flag;
}

#region Default options

[NotNull]
public static T None => Options.First();

[NotNull]
public static T Max => Options.Last();

[NotNull]
public static IEnumerable<T> All => Options;

#endregion

[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }

public override bool IsBit => (Flag & (Flag - 1)) == 0;

#endregion

[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);

#region Factories

[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}

var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);

return newOption;
}

[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}

private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}

public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}

[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));

return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}

// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}

var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}

private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option)
{
return this | option;
}

public T Reset(Option<T> option)
{
return this ^ option;
}

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:names}";

public bool Contains(T option) => Contains(option.Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

public int CompareTo(Option<T> other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

#region IEquatable

public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;

public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;

public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;

[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);

[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);

private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}

#endregion

private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}


A new option-set can now be defined by deriving it from Option<T> and adding static properties for the desired flags:



public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();

public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}


Since there is only one option-class now, tests have also become simpler.



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);

var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);

var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);

var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";

Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);

Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}




The intended usage is:




  • Logger layer where the user can define their custom log-levels

  • FeatureService where the user can define their custom behaviours

  • Other services that work with some default options and let the user customize it with their domain-specific flags.






share|improve this answer











$endgroup$



(self-answer)





v3



I wanted to use v2 of this code (below) to upgrade my old MimeType that was very similar but it turned out I cannot because I need string values (like application/json) and not numerical ones (like 1) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString and replaced binary operations with HashSets. Alternatively this could use a generic value but currently I don't see any use for them.



[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);

public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract IImmutableSet<SoftString> Values { get; }

public abstract bool IsFlag { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);

// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;

static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);

// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}

protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}

[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));

/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }

/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

public override IImmutableSet<SoftString> Values { get; }

/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;

#endregion

#region Factories

public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}

[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}

if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}

var newOption = New(name, values);

if (name == Unknown)
{
return newOption;
}

Known = Known.Add(newOption);
return newOption;
}

private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}

[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}

[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));

return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}

private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option) => this | option;

public T Reset(Option<T> option) => this ^ option;

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:asc}";

public bool Contains(T option) => Values.Overlaps(option.Values);

#region IEquatable

public bool Equals(Option<T> other) => Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}

private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}

#endregion
}




v2



I have made a couple of changes so here's the summary and the improved code:




  • Using CallerMemberName for automatic option names, however, it's still possible to create custom options with anyname.

  • Using generic Option<T> to remove the Dictionary and provide a few default properties such as None, All or Max, Bits.

  • Cleaned-up naming; now parsing APIs are called FromName and FromValue

  • Added internal set of options so that I can check whether an option is already defined and use it for other properties like All, Max and Bits.

  • Added multi-bit support.

  • Not using BitVector32 yet... maybe later.

  • Added IFormattable interface and three formats: names, flags and names+flags.

  • Encapsulated operators | and ^ respectively as Set and Reset.

  • Added Flags property that enumerates all bits of an option.


[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));

// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }

[NotNull]
public abstract SoftString Name { get; }

public abstract int Flag { get; }

/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}

[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);

private static readonly OptionComparer Comparer = new OptionComparer();

private static IImmutableSet<T> Options;

static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}

protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");

Name = name;
Flag = flag;
}

#region Default options

[NotNull]
public static T None => Options.First();

[NotNull]
public static T Max => Options.Last();

[NotNull]
public static IEnumerable<T> All => Options;

#endregion

[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);

#region Option

public override SoftString Name { [DebuggerStepThrough] get; }

[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }

public override bool IsBit => (Flag & (Flag - 1)) == 0;

#endregion

[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);

#region Factories

[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}

var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);

return newOption;
}

[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}

private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}

public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}

[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));

return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}

[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}

// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}

var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}

private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}

#endregion

public T Set(Option<T> option)
{
return this | option;
}

public T Reset(Option<T> option)
{
return this ^ option;
}

[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}

if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}

return ToString();
}

public override string ToString() => $"{this:names}";

public bool Contains(T option) => Contains(option.Flag);

public bool Contains(int flags) => (Flag & flags) == flags;

public int CompareTo(Option<T> other) => Comparer.Compare(this, other);

public int CompareTo(object other) => Comparer.Compare(this, other);

#region IEquatable

public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);

public override bool Equals(object obj) => Equals(obj as Option<T>);

public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);

#endregion

#region Operators

public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));

public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));

public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;

public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);

public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;

public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;

public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;

public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;

[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);

[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);

private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}

#endregion

private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}

public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}


A new option-set can now be defined by deriving it from Option<T> and adding static properties for the desired flags:



public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }

/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();

/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();

/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();

public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}


Since there is only one option-class now, tests have also become simpler.



public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));

Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);

var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);

var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);

var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";

Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);

Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}




The intended usage is:




  • Logger layer where the user can define their custom log-levels

  • FeatureService where the user can define their custom behaviours

  • Other services that work with some default options and let the user customize it with their domain-specific flags.







share|improve this answer














share|improve this answer



share|improve this answer








edited Jun 16 at 15:55

























answered May 27 at 17:18









t3chb0tt3chb0t

38.6k7 gold badges62 silver badges142 bronze badges




38.6k7 gold badges62 silver badges142 bronze badges















  • $begingroup$
    Does a private protected constructor work? I have never seen this before.
    $endgroup$
    – dfhwze
    Jun 16 at 16:03






  • 1




    $begingroup$
    @dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
    $endgroup$
    – t3chb0t
    Jun 16 at 16:07


















  • $begingroup$
    Does a private protected constructor work? I have never seen this before.
    $endgroup$
    – dfhwze
    Jun 16 at 16:03






  • 1




    $begingroup$
    @dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
    $endgroup$
    – t3chb0t
    Jun 16 at 16:07
















$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03




$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03




1




1




$begingroup$
@dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07




$begingroup$
@dfhwze this is new in 7.2 and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T> can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07



















draft saved

draft discarded



















































Thanks for contributing an answer to Code Review Stack Exchange!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


Use MathJax to format equations. MathJax reference.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221057%2fgeneral-purpose-replacement-for-enum-with-flagsattribute%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

He _____ here since 1970 . Answer needed [closed]What does “since he was so high” mean?Meaning of “catch birds for”?How do I ensure “since” takes the meaning I want?“Who cares here” meaningWhat does “right round toward” mean?the time tense (had now been detected)What does the phrase “ring around the roses” mean here?Correct usage of “visited upon”Meaning of “foiled rail sabotage bid”It was the third time I had gone to Rome or It is the third time I had been to Rome

Bunad

Færeyskur hestur Heimild | Tengill | Tilvísanir | LeiðsagnarvalRossið - síða um færeyska hrossið á færeyskuGott ár hjá færeyska hestinum