Search code examples
c#genericscontravariance

Getting generic class assigned to given object type


I'm writing a C# program that calculates total worth of financial assets. Each asset is represented by asset ID. There are calculator classes specialized for calculating worth of specific asset ID.

public interface IAssetId
{
    string GetSymbol();
}

public class Currency(string currency) : IAssetId
{
    public string GetSymbol() => currency;
}

public class EtfTicker(string ticker) : IAssetId
{
    public string GetSymbol() => ticker;
}

public interface IAssetWorthCalculator<in TAssetId> where TAssetId : IAssetId 
{
    decimal CalculateWorth(TAssetId assetId);
}

public class MoneyWorthCalculator : IAssetWorthCalculator<Currency>
{
    public decimal CalculateWorth(Currency assetId) ...
}

public class EtfWorthCalculator : IAssetWorthCalculator<EtfTicker>
{
    public decimal CalculateWorth(EtfTicker assetId) ...
}

Now let's say I have a list of different asset IDs for which I want to calculate total worth.

List<IAssetId> assetIds = [
    new Currency("USD"),
    new EtfTicker("SPY")
];

decimal worth = TotalWorthCalculator.GetTotalWorth(assetIds);

The total worth calculations would be performed in a class that would look something like this:

public class TotalWorthCalculator
{
    public decimal GetTotalWorth(IList<IAssetId> assetIds)
    {
        decimal sum = 0;
        foreach (IAssetId id in assetIds)
        {
            IAssetWorthCalculator<IAssetId> worthCalculator = ???
            decimal worth = worthCalculator.CalculateWorth(id);
            sum += worth;
        }

        return sum;
    }
}

I'm trying to figure out a way to implement a class, that returns a proper worth calculator for given asset ID e.g.

  • if the asset ID is currency, then the class returns MoneyWorthCalculator object
  • if the asset ID is ETF, then it returns EtfWorthCalculator

Problem 1: I can't get a common type for the worth calculators (to use it in a class that returns proper calculators for given asset ID). So trying to do the following raises a compiler error (because IAssetId is a more base type than Currency in MoneyWorthCalculator):

IAssetWorthCalculator<IAssetId> worthCalculator = new MoneyWorthCalculator();

Problem 2: Each calculator has a CalculateWorth method, that accepts only a specific parameter type, so MoneyWorthCalculator won't accept IAssetId.

I was thinking about using a non-generic versions of the worth calculators, but that would require method parameter type checking, which would break the Liskov Substitution Principle:

public class MoneyWorthCalculator : IAssetWorthCalculator
{
    public decimal CalculateWorth(IAssetId assetId)
    {
        if (assetId is not Currency)
        {
            throw new ArgumentException();
        }

        return 10;
    }
}

Solution

  • Without making changes to the defined abstractions, you can change the TotalWorthCalculator to the following:

    public class TotalWorthCalculator
    {
        private readonly IList<object> calculators;
    
        public TotalWorthCalculator(IList<object> calculators)
        {
            this.calculators = calculators;
        }
    
        public decimal GetTotalWorth(IList<IAssetId> assetIds) =>
            assetId.Select(this.Calculate).Sum();
        
        private double Calculate(IAssetId id)
        {
            Type calculatorType =
                typeof(IAssetWorthCalculator<>).MakeGenericType(id.GetType());
        
            dynamic calculator = this.calculators
                .Single(c => calculatorType.IsAssignableFrom(c.GetType());
                
            return calculator.CalculateWorth((dynamic)id);
        }
    }
    

    This solution uses Reflection and dynamic typing. You can instantiate the TotalWorthCalculator as follows:

    var calculator = new TotalWorthCalculator(new List<object>()
    {
        new MoneyWorthCalculator(),
        new EtfWorthCalculator()
    });
    

    Things to consider:

    • Instead of solely having a generic IAssetWorthCalculator<T> interface, you might let it inherit from a non-generic IAssetWorthCalculator interface. Although this makes the implementations slightly more complicated, it prevents the use of dynamic typing. It also allows you to inject the more pleasant IListinto theTotalWorthCalculator's constructor, compared to the more obscure IList. This will also simplify composing the TotalWorthCalculatorusing a DI Container, as they will have trouble resolving anIList` for you as the type isn't specific.
    • Dynamic typing is simply a form of Reflection, so please be aware that the code will keep compiling, but will start failing at runtime when you change the signature of the IAssetWorthCalculator<T>.CalculateWorth method. Also note that calling .CalculateWorth using dynamic will fail on any type that isn't defined with the public keyword. I consider this a bug in the C# compiler's runtime code, but is probably a security feature. If you stick with using dynamic typing, make sure to add some unit tests to your project testing the correctness of the TotalWorthCalculator.
    • Another option to get rid of the dynamic typing, while the non-generic IAssetWorthCalculator isn't suitable, is to use a generic adapter that wraps a generic IAssetWorthCalculator<T> while implementing a non-generic interface, e.g. ICalculationAdapter { double Calculate(IAssetId id); }. This will still require some reflection to create new CalculationAdapter<T> instances, but at least makes the solution safe from Refactoring.
    • In case you stick with the use of