Enum classes instead of enum
We usually define an enumeration to represent various state values until I see this in Java. Using enumerations, I would like to think about writing in C#, and share my feelings today.
1. usually we use enum like this
(1) use enum in switch
public enum EmployeeType { Manager, Servant, AssistantToTheRegionalManager }
public class Employee { public EmployeeType Type { get; set; } public decimal Bonus { get; set; } }
static void ProcessBonus(Employee employee) { switch (employee.Type) { case EmployeeType.Manager: employee.Bonus = 1000m; break; case EmployeeType.Servant: employee.Bonus = 0.01m; break; case EmployeeType.AssistantToTheRegionalManager: employee.Bonus = 1.0m; break; default: throw new ArgumentOutOfRangeException(); } }
I used to write code like this. It is very bad. It violates the principle of opening and closing, and the scalability is very poor. This is not allowed in code specifications.
(2) Type conversion
EnumTricks.IsVolumeHigh((Volume)27); EnumTricks.High((int)Medium);
2. the shortcomings of the enum
What is said about the enumerated MSDN documentation:
“The enum keyword is used to declare an enumeration, a distinct type that consists of a set of named constants called the enumerator list. Every enumeration type has an underlying type, which can be any integral type except char. The default underlying type of the enumeration elements is int. By default, the first enumerator has the value 0, and the value of each successive enumerator is increased by 1.
(1) No type safety
public enum Volume { Low = 1, Medium, High }
public static class EnumTricks { public static bool IsVolumeHigh(Volume volume) { var result = false; switch (volume) { case Volume.Low: Console.WriteLine("Volume is low."); break; case Volume.Medium: Console.WriteLine("Volume is medium."); break; case Volume.High: Console.WriteLine("Volume is high."); result = true; break; } return result; } }
static void Main(string[] args) { EnumTricks.IsVolumeHigh((Volume)27); Console.ReadKey(); }
public static class EnumTricks { public static bool IsVolumeHigh(Volume volume) { var result = false; switch (volume) { case Volume.Low: Console.WriteLine("Volume is low."); break; case Volume.Medium: Console.WriteLine("Volume is medium."); break; case Volume.High: Console.WriteLine("Volume is high."); result = true; break; } return result; } public static int EnumToInt(Volume volume) { return (int)volume; } public static Volume IntToEnum(int intValue) { return (Volume)intValue; } public static Volume StringToEnum(string stringValue) { return (Volume)Enum.Parse(typeof(Volume), stringValue); } public static int StringToInt(string stringValue) { var volume = StringToEnum(stringValue); return EnumToInt(volume); } public static string EnumToString(Volume volume) { return volume.ToString(); } }
This should fail, at least at runtime. It does not. This is really weird… no bad calls are detected during compilation or during runtime. You will feel that you are in a false state of security. If we convert the passed enumeration into a string, let’s see how the two cases differ:
I don’t know if you are consciously checking if the incoming value is a valid value when you use the enumeration. You can use Enum.IsDefined() to check if the int value is a valid value.
Solution: If the int value is within the definition of the enumeration value, use Enum.IsDefined() to find it. Returns True if it is in range, otherwise returns False.
(2) conversion
Have you tried converting enum to int, int to enum, string to enum, and string to enum int? The following code:
public static class EnumTricks { public static bool IsVolumeHigh(Volume volume) { var result = false; switch (volume) { case Volume.Low: Console.WriteLine("Volume is low."); break; case Volume.Medium: Console.WriteLine("Volume is medium."); break; case Volume.High: Console.WriteLine("Volume is high."); result = true; break; } return result; } public static int EnumToInt(Volume volume) { return (int)volume; } public static Volume IntToEnum(int intValue) { return (Volume)intValue; } public static Volume StringToEnum(string stringValue) { return (Volume)Enum.Parse(typeof(Volume), stringValue); } public static int StringToInt(string stringValue) { var volume = StringToEnum(stringValue); return EnumToInt(volume); } }
Isn’t our daily code also has such type conversion code, not to say bad, but type conversion is also a performance loss, if you can change the way can be achieved and avoid the above problem is not better, so our code It is also better to maintain and extend. Let’s solve this problem by using enumeration classes.
3. use enumeration classes instead of enum
public class Enumeration: IComparable { private readonly int _value; private readonly string _displayName; protected Enumeration() { } protected Enumeration(int value, string displayName) { _value = value; _displayName = displayName; } public int Value { get { return _value; } } public string DisplayName { get { return _displayName; } } public override string ToString() { return DisplayName; } public static IEnumerable<T> GetAll<T>() where T : Enumeration, new() { var type = typeof(T); var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); foreach (var info in fields) { var instance = new T(); var locatedValue = info.GetValue(instance) as T; if (locatedValue != null) { yield return locatedValue; } } } public override bool Equals(object obj) { var otherValue = obj as Enumeration; if (otherValue == null) { return false; } var typeMatches = GetType().Equals(obj.GetType()); var valueMatches = _value.Equals(otherValue.Value); return typeMatches && valueMatches; } public override int GetHashCode() { return _value.GetHashCode(); } public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) { var absoluteDifference = Math.Abs(firstValue.Value - secondValue.Value); return absoluteDifference; } public static T FromValue<T>(int value) where T : Enumeration, new() { var matchingItem = parse<T, int>(value, "value", item => item.Value == value); return matchingItem; } public static T FromDisplayName<T>(string displayName) where T : Enumeration, new() { var matchingItem = parse<T, string>(displayName, "display name", item => item.DisplayName == displayName); return matchingItem; } private static T parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration, new() { var matchingItem = GetAll<T>().FirstOrDefault(predicate); if (matchingItem == null) { var message = string.Format("'{0}' is not a valid {1} in {2}", value, description, typeof(T)); throw new ApplicationException(message); } return matchingItem; } public int CompareTo(object other) { return Value.CompareTo(((Enumeration)other).Value); } }
public class Volume: Enumeration { private Volume() { throw new Exception(""); } private Volume(int value, string displayName): base(value, displayName) { } public static readonly Volume Low = new Volume(1, nameof(Low).ToLowerInvariant()); public static readonly Volume Medium = new Volume(2, nameof(Medium).ToLowerInvariant()); public static readonly Volume High = new Volume(3, nameof(High).ToLowerInvariant()); public static IEnumerable<Volume> List() => new[] { Low, Medium, High }; public static Volume From(int value) { var state = List().SingleOrDefault(s => s.Value == value); if (state == null) { throw new Exception($"Possible values for Volume: {String.Join(",", List().Select(s => s.Value))}"); } return state; } public static Volume FromName(string name) { var state = List() .SingleOrDefault(s => String.Equals(s.DisplayName, name, StringComparison.CurrentCultureIgnoreCase)); if (state == null) { throw new Exception($"Possible values for Volume: {String.Join(",", List().Select(s => s.DisplayName))}"); } return state; } }
static void Main(string[] args) { //EnumTricks.IsVolumeHigh((Volume)27); //var tmp = Enum.IsDefined(typeof(Volume), 3); //var str = EnumTricks.EnumToString((Volume)27); //var str2 = EnumTricks.EnumToString((Volume)3); //Console.WriteLine($"Volume 27:{str}"); //Console.WriteLine($"Volume 3:{str2}"); Console.WriteLine("------------------------------------------------------------"); Console.WriteLine(Volume.High.Value); Console.WriteLine(Volume.High.DisplayName); var volume = Volume.From(2); var volume2 = Volume.FromName("high"); var none = Volume.From(27); Console.ReadKey(); }
4. application
code show as below:
Under the Error file:
public interface ICommonError { int GetErrCode(); string GetErrMsg(); ICommonError SetErrMsg(string errMsg); }
public class Enumeration : IComparable { private readonly int _value; private readonly string _displayName; protected Enumeration() { } protected Enumeration(int value, string displayName) { _value = value; _displayName = displayName; } public int Value { get { return _value; } } public string DisplayName { get { return _displayName; } } public override string ToString() { return DisplayName; } public static IEnumerable<T> GetAll<T>() where T : Enumeration, new() { var type = typeof(T); var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); foreach (var info in fields) { var instance = new T(); var locatedValue = info.GetValue(instance) as T; if (locatedValue != null) { yield return locatedValue; } } } public override bool Equals(object obj) { var otherValue = obj as Enumeration; if (otherValue == null) { return false; } var typeMatches = GetType().Equals(obj.GetType()); var valueMatches = _value.Equals(otherValue.Value); return typeMatches && valueMatches; } public override int GetHashCode() { return _value.GetHashCode(); } public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) { var absoluteDifference = Math.Abs(firstValue.Value - secondValue.Value); return absoluteDifference; } public static T FromValue<T>(int value) where T : Enumeration, new() { var matchingItem = parse<T, int>(value, "value", item => item.Value == value); return matchingItem; } public static T FromDisplayName<T>(string displayName) where T : Enumeration, new() { var matchingItem = parse<T, string>(displayName, "display name", item => item.DisplayName == displayName); return matchingItem; } private static T parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration, new() { var matchingItem = GetAll<T>().FirstOrDefault(predicate); if (matchingItem == null) { var message = string.Format("'{0}' is not a valid {1} in {2}", value, description, typeof(T)); throw new ApplicationException(message); } return matchingItem; } public int CompareTo(object other) { return Value.CompareTo(((Enumeration)other).Value); } }
public class EmBusinessError : Enumeration, ICommonError { private int errCode; private String errMsg; public static readonly EmBusinessError parameterValidationError = new EmBusinessError(10001, "The parameter is invalid"); private EmBusinessError() { throw new Exception("Private constructor cannot be called"); } private EmBusinessError(int value, string displayName) : base(value, displayName) { this.errCode = value; this.errMsg = displayName; } public int GetErrCode() { return this.errCode; } public string GetErrMsg() { return this.errMsg; } public void SetErrCode(int errCode) { this.errCode = errCode; } public ICommonError SetErrMsg(string errMsg) { this.errMsg = errMsg; return this; } }
//Wrapper business exception class implementation public class BusinessException : Exception, ICommonError { private ICommonError commonError; //Passing parameters directly receiving EmBusinessError for constructing business exceptions public BusinessException(ICommonError commonError):base() { this.commonError = commonError; } public BusinessException(ICommonError commonError, string errMsg):base() { this.commonError = commonError; this.commonError.SetErrMsg(errMsg); } public int GetErrCode() { return this.commonError.GetErrCode(); } public string GetErrMsg() { return this.commonError.GetErrMsg(); } public ICommonError SetErrMsg(string errMsg) { this.commonError.SetErrMsg(errMsg); return this; } public ICommonError GetCommonError() { return commonError; } }
Exception middleware:
public class ExceptionHandlerMiddleWare { private readonly RequestDelegate next; /// <summary> /// /// </summary> /// <param name="next"></param> public ExceptionHandlerMiddleWare(RequestDelegate next) { this.next = next; } public async Task Invoke(HttpContext context) { try { await next(context); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private static async Task HandleExceptionAsync(HttpContext context, Exception exception) { if (exception == null) return; await WriteExceptionAsync(context, exception).ConfigureAwait(false); } private static async Task WriteExceptionAsync(HttpContext context, Exception exception) { var response = context.Response; response.ContentType = "application/json;charset=utf-8"; var result = new CommonReturnType(); if (exception is BusinessException) { var businessException = (BusinessException)exception; var errModel = new { errCode= businessException.GetErrCode(), errMsg= businessException.GetErrMsg() }; result = CommonReturnType.Create(errModel, "fail"); } await response.WriteAsync(JsonConvert.SerializeObject(new { data = result.GetData(), status = result.GetStatus() }) ).ConfigureAwait(false); } }
Response folder:
public class CommonReturnType { //Indicates the return processing result of the corresponding request "success" or "fail" private string status; //If status=success, the data returned in the front end requires json data. //If status=fail, a common error code format is used in data. private object data; //定义一个通用的创建方法 public static CommonReturnType Create(object result) { return CommonReturnType.Create(result, "success"); } public static CommonReturnType Create(object result, string status) { CommonReturnType type = new CommonReturnType(); type.SetStatus(status); type.SetData(result); return type; } public string GetStatus() { return status; } public void SetStatus(string status) { this.status = status; } public object GetData() { return data; } public void SetData(object data) { this.data = data; } }