General purpose replacement for enum with FlagsAttribute

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 convinient?
  • What do you think about the automatic Flag maintenance and options creation?