top of page

Create enhanced enum's by using enumeration classes instead of enum types.

If you have been developing in C# for any period of time, you are used to enums. A enumeration type (or enum type) is simply a value type defined by a set of named constants of the underlying integral numeric type. Take currency for example:


public enum CurrencyType
{
  	None = 0,
 	USD = 1,
  	GBP = 2,
  	EUR = 3
}

When you need a predefined list of values which represent some kind of variable that can only take one of a small set of possible values, enums are awesome. Given that the underlying data in your database would be saved as a int, you have an optimised design whilst leveraging readability of your code because you don't even need to remember that USD is 1. What can be better than writing CurrencyType.USD after all. Consider the following business case:


A customer pays $200 for a product priced at £109.99. What change must we give him in our local currency and in what denominations?

For the impatient: TL;DR

The GitHub Project is at the bottom of the page


The challenge comes when you want to store relationships where you have multiple groups of values such as currency denominations of the currencies. Using enums for control flow or more robust abstractions can be a code smell. This type of usage leads to fragile code with many control flow statements checking values of the enum. Luckily, given that C# provides a rich set of features as a object-oriented language, enabling you to create Enumeration classes to solve the problem. Think of them like Extended Enums. I first came across the concept of a Enhanced Enum a while back in John Skeet's coding blog. The idea is to implement an Enumeration base class.



public abstract class ExtendedEnum<T> where T : Enum
{
   public string Name { get; }
   public int Id { get; }
   public object Value { get; }
   public T Filter { get; }
   
   protected ExtendedEnum()
   {
   }

   protected ExtendedEnum(int id, string name, object value, T filter)
   {
      Id = id;
      Name = name;
      Value = value;
      Filter = filter;
   }
        
   public override string ToString()
   {
      return Name;
   }

   public int CompareTo(object obj)
   {
      return Id.CompareTo(((ExtendedEnum<T>)obj).Id);
   }
        
   public override bool Equals(object obj)
   {
      return obj is ExtendedEnum<T> enumeration &&
      Name == enumeration.Name &&
      Id == enumeration.Id &&
      enumeration.Filter.Equals(Filter);
   }

   public override int GetHashCode()
   {
      return HashCode.Combine(Name, Id, Filter);  
   }
}

Now lets leverage our Extended Enum to create a change converter. To solve that we have a CurrencyTypeFilter. This is based on the fact that we want to group our currency denominations by type, for example:


public enum CurrencyTypeFilter
{
   [Description("None")]
   None = 0,
   [Description("United States Dollar")]
   USD = 1,
   [Description("Pound Sterling")]
   GBP = 2,
   [Description("Euro")]
   EUR = 3

}

As we want to save the change given as integers in our database to derive the benefit of using enum's as discussed at the beginning of the article, we also create a way to get the values by id, retrieve the filtered currency list etc.


public interface ICurrencyType
{
   CurrencyType FromString(string currencyString);
   CurrencyType FromId(int id);
   IEnumerable<CurrencyType> List(CurrencyTypeFilter filter = CurrencyTypeFilter.None);
   decimal GetDecimalValue(CurrencyType currencyType);
   decimal GetCurrencyTypeFilterId(CurrencyType currencyType);
   string ToString();
}

Finally, we need to create our CurrencyType. I have created it as sealed class to remove inheritance from the class as users shouldn't derive a class from it. Instead it is implemented as a thread safe singleton that is created so that singleton property is maintained even in multithreaded environment.


public sealed class CurrencyType : ExtendedEnum<CurrencyTypeFilter>, ICurrencyType
{
   private static readonly Lazy<CurrencyType> Lazy = new(() => new CurrencyType());
   public static CurrencyType Instance => Lazy.Value;
   public List<CurrencyType> CurrencyTypeList { get; }

   private CurrencyType()
   {
      CurrencyTypeList = new List<CurrencyType>
      {
         //United States Dollar
         new(101, "$100", 100.00M, CurrencyTypeFilter.USD),
         new(102, "$50", 50.00M, CurrencyTypeFilter.USD),
         new(103, "$20", 20.00M, CurrencyTypeFilter.USD),
         new(104, "$10", 10.00M, CurrencyTypeFilter.USD),
         new(105, "$5", 5.00M, CurrencyTypeFilter.USD),
         new(106, "$2", 2.00M, CurrencyTypeFilter.USD),
         new(107, "$1", 1.00M, CurrencyTypeFilter.USD),
         new(108, "50¢", 0.50M, CurrencyTypeFilter.USD),
         new(109, "25¢", 0.25M, CurrencyTypeFilter.USD),
         new(110, "10¢", 0.10M, CurrencyTypeFilter.USD),
         new(111, "5¢", 0.05M, CurrencyTypeFilter.USD),
         new(112, "1¢", 0.01M, CurrencyTypeFilter.USD),
         //Pound Sterling 
         new(201, "£50", 50.00M, CurrencyTypeFilter.GBP),
         new(202, "£20", 20.00M, CurrencyTypeFilter.GBP),
         new(203, "£10", 10.00M, CurrencyTypeFilter.GBP),
         new(204, "£5", 5.00M, CurrencyTypeFilter.GBP),
         new(205, "£2", 2.00M, CurrencyTypeFilter.GBP),
         new(206, "£1", 1.00M, CurrencyTypeFilter.GBP),
         new(207, "50p", 0.50M, CurrencyTypeFilter.GBP),
         new(208, "20p", 0.20M, CurrencyTypeFilter.GBP),
         new(209, "10p", 0.10M, CurrencyTypeFilter.GBP),
         new(210, "5p", 0.05M, CurrencyTypeFilter.GBP),
         new(211, "2p", 0.02M, CurrencyTypeFilter.GBP),
         new(212, "1p", 0.01M, CurrencyTypeFilter.GBP),
         //Euro
         new(301, "€500", 500.00M, CurrencyTypeFilter.EUR),
         new(302, "€200", 200.00M, CurrencyTypeFilter.EUR),
         new(303, "€100", 100.00M, CurrencyTypeFilter.EUR),
         new(304, "€50", 50.00M, CurrencyTypeFilter.EUR),
         new(305, "€20", 20.00M, CurrencyTypeFilter.EUR),
         new(306, "€10", 10.00M, CurrencyTypeFilter.EUR),
         new(307, "€5", 5.00M, CurrencyTypeFilter.EUR),
         new(308, "€2", 2.00M, CurrencyTypeFilter.EUR),
         new(309, "€1", 1.00M, CurrencyTypeFilter.EUR),
         new(310, "50¢", 0.50M, CurrencyTypeFilter.EUR),
         new(311, "20¢", 0.20M, CurrencyTypeFilter.EUR),
         new(312, "10¢", 0.10M, CurrencyTypeFilter.EUR),
         new(313, "5¢", 0.05M, CurrencyTypeFilter.EUR),
         new(314, "2¢", 0.02M, CurrencyTypeFilter.EUR),
         new(315, "1¢", 0.01M, CurrencyTypeFilter.EUR)
      };


   }


   private CurrencyType(int id, string name, decimal value, CurrencyFormat format, CurrencyTypeFilter currencyTypeFilter) : base(id, name, currencyTypeFilter)
   {

   }

   public CurrencyType FromString(string currencyString)
   {
      return List().Single(r => string.Equals(r.Name, currencyString, StringComparison.OrdinalIgnoreCase));
   }

   public CurrencyType FromId(int id)
   {
      return List().Single(r => r.Id == id);
   }


   public IEnumerable<CurrencyType> List(CurrencyTypeFilter currencyTypeFilter = CurrencyTypeFilter.None)
   {
      return currencyTypeFilter == CurrencyTypeFilter.None ? CurrencyTypeList : CurrencyTypeList.Where(currencyType => currencyType.Filter == currencyTypeFilter);
   }

   public decimal GetDecimalValue(CurrencyType currencyType)
   {
      return Convert.ToDecimal(currencyType.Value);
   }

   public decimal GetCurrencyTypeFilterId(CurrencyType currencyType)
   {
      return (int)currencyType.Filter;
   }
   public override string ToString()
   {
      return Name;
   }
    
}

In order to handle a payment, I have created a Payment class implementing the builder pattern as described in this blog post. This class consists of the amount to be paid and its currency type, the amount submitted for payment and its currency type, the amount of change and the change denominations.


public class Payment
{
   public decimal Amount { get; private set; }
   public decimal AmountSubmitted { get; private set; }
   public CurrencyTypeFilter CurrencyType { get; private set; }
   public decimal Change { get; set; }
   public CurrencyTypeFilter ChangeCurrencyType { get; set; }
   public Dictionary<CurrencyType, int> Denominations { get; set; }
   private Payment() { }
   public static PaymentBuilder Builder => new PaymentBuilder();
   public class PaymentBuilder
   {
      private decimal _amount;
      public PaymentBuilder Amount(decimal amount)
      {
         _amount = amount;
         return this;
      }
      private decimal _amountSubmitted;
      public PaymentBuilder AmountSubmitted(decimal amountSubmitted)
      {
         _amountSubmitted = amountSubmitted;
         return this;
      }
      private CurrencyTypeFilter _currencyType;
      public PaymentBuilder CurrencyType(CurrencyTypeFilter currencyType)
      {
         _currencyType = currencyType;
         return this;
      }
      private decimal _change;
      public PaymentBuilder Change(decimal change)
      {
         _change = change;
         return this;
      }
      private CurrencyTypeFilter _changeCurrencyType;
      public PaymentBuilder ChangeCurrencyType(CurrencyTypeFilter changeCurrencyType)
      {
         _changeCurrencyType = changeCurrencyType;
         return this;
       }
       private Dictionary<CurrencyType, int> _denominations;
       public PaymentBuilder Denominations(Dictionary<CurrencyType, int> denominations)
       {
          _denominations = denominations;
          return this;
       }
       public Payment Build()
       {
          Validate();
          return new Payment
          {
             Amount = _amount,
             AmountSubmitted = _amountSubmitted,
             CurrencyType = _currencyType,
             Change = _change,
             ChangeCurrencyType = _changeCurrencyType,
             Denominations = _denominations,
           };
       }
       public void Validate()
       {
          void AddError(Dictionary<string, string> items, string property, string message)
          {
             if (items.TryGetValue(property, out var error))
                items[property] = $"{error}\n{message}";
             else
                items[property] = message;
           }
           var errors = new Dictionary<string, string>();
           if (_amount == default) AddError(errors, "Amount", "Value is required");
           if (_amountSubmitted == default) AddError(errors, "AmountSubmitted", "Value is required");
           if (_currencyType == default) AddError(errors, "CurrencyType", "Value is required");
           if (_amountSubmitted < _amount)
           {
              AddError(errors, "AmountSubmitted", "AmountSubmitted must be equal to or more than amount required");
           }

           if (errors.Count > 0)
            throw new BuilderException(errors);
           }
   }
}

To process a payment, it is as simple as creating a payment and building up the change values.


var payment = Payment.Builder.Amount(amountToBePaid).CurrencyType(paymentCurrency).AmountSubmitted(paymentAmount);
payment = ProcessPayment(payment, currencyType);

The Process payment method leverages the GetDecimalValue to determine the change after filtering the Currency List by the Currency Type Filter.


private static Payment ProcessPayment(Payment payment, CurrencyType currencyType)
{
	var change = payment.AmountSubmitted - payment.Amount;
        payment.ChangeCurrencyType = payment.CurrencyType;
        payment.Change = change;
        payment.Denominations = new Dictionary<CurrencyType, int>();
        var currencyList = currencyType.List(payment.CurrencyType);
        foreach (var currency in currencyList)
        {
   	var currencyValue = currency.GetDecimalValue(currency);
              var changeCount = (int)(change / currencyValue);
              if (changeCount <= 0) continue;
              payment.Denominations.Add(currency, changeCount);
              change %= currencyValue;
   	}
        return payment;
}

The end result is a response as shown below. The product is priced at $5559.50. As payment is made in Pounds, the customer pays £4420. The customer would need to receive £417.35 in change made up of 8 £50 notes, 1 £10 note, 1 £5 note, 1 £2 coin, 1 20p coin and 1 5p coin. Now we may want to mark in the database that the payment was made in pounds. You can get the underlying filter id by calling GetCurrencyTypeFilterId which in the case of Pounds is 2.


The sample code is available at Netizine/CurrencyDemo (github.com)

Comentários


bottom of page