Search code examples
c#linq

Is there a cleaner way to sum all numerical properties in a class individually?


I have a class, represented below as Example class. In my DoSomeLogic Method, I need to sum a collection of Example class, and want to return a new instance of Example class where each numerical property contains the sum of that property from the collection.

Below is an example of how i would typically do this, but my problem is that I am continuously updating my example class, adding new properties etc. When doing this I need to remember to also update the sum method for example class too, or my code is broken, which seems like a hurdle/vulnerability I'd like to avoid.

Can anyone show me a better way to do this generically, so i don't need to update the sum method if i add a new numerical property to the example class?

public class Example 
{
    public Example()
    {
    }

    public Example(int a, int b, float c)
    {
        A = a;
        B = b;
        C = c;
        IgnoreThis = "Non numerical properties should be ignored by the sum method";
    }

    public int A { get; set; }
    public int B { get; set; }
    public float C { get; set; }
    public string IgnoreThis {get; set;}
}
using System.Collections.Generic;
using System.Linq;

public static class Extension
{
    public static Example Sum(this IEnumerable<Example> source)
    {
        Example result = new Example();
        result.A = source.Sum(x => x.A);
        result.B = source.Sum(x => x.B);
        result.C = source.Sum(x => x.C);
        return result;
    }
}
using System.Collections.Generic;

public class Usage
{
    private List<Example> Examples = new List<Example>();

    public void DoSomeLogic()
    {
        Examples.Add(new Example(1, 2, 3.75f));
        Examples.Add(new Example(2, 3, 6.25f));
        Example SumOfEachIndividualProperty = Examples.Sum();
        //Expected result from logging SumOfEachIndividualProperty.A is 3;
        //Expected result from logging SumOfEachIndividualProperty.B is 5;
        //Expected result from logging SumOfEachIndividualProperty.C is 10;
    }
}

Solution

  • You could solve this using reflection, but I think that's overkill (and would not be very performant).

    You could consider putting the "add" functionality into the class itself:

    public class Example
    {
        public Example()
        {
        }
    
        public Example(int a, int b, float c)
        {
            A = a;
            B = b;
            C = c;
            IgnoreThis = "Non numerical properties should be ignored by the sum method";
        }
    
        public void Add(Example other)
        {
            A += other.A;
            B += other.B;
            C += other.C;
        }
    
        public int    A          { get; set; }
        public int    B          { get; set; }
        public float  C          { get; set; }
        public string IgnoreThis { get; set; }
    }
    

    Then your extension method would become:

    public static Example Sum(this IEnumerable<Example> source)
    {
        var result = new Example();
    
        foreach (var example in source)
        {
            result.Add(example);
        }
    
        return result;
    }
    

    When you add new members to the Example class you would need to update the Add() method accordingly, but you would not need to modify the extension method.


    ADDENDUM: Since someone was wondering about using INumber in the implementation that uses reflection, I thought I'd have a go.

    Firstly, here's a example class that you want to sum the properties for:

    public class Example
    {
        public Example()
        {
        }
    
        public Example(int a, int b, float c)
        {
            A = a;
            B = b;
            C = c;
            IgnoreThis = "Non numerical properties should be ignored by the sum method";
        }
    
        public override string ToString()
        {
            return $"A={A}, B={B}, C={C}";
        }
    
        public int    A { get; set; }
        public int    B { get; set; }
        public float  C { get; set; }
        public string IgnoreThis { get; set; } = "";
    }
    

    Here's a sample program that demonstrates how we want to add up the properties for all the items in a collection. Note the expected output:

    public static class Program
    {
        public static void Main()
        {
            var items = new[]
            {
                new Example(1, 4, 7),
                new Example(2, 5, 8),
                new Example(3, 6, 9)
            };
    
            var totals = Extension.SumNumericProperties(items);
    
            Console.WriteLine(totals); // A=6, B=15, C=24
        }
    }
    

    And here's how I implemented the Extension class:

    If you need to add additional numeric types (e.g. decimal) you'd just need to add an new numericProperties item with the required type and add calls to sumValueTo() and assignResults().

    public static class Extension
    {
        public static T SumNumericProperties<T>(IEnumerable<T> items) where T: new()
        {
            var intAdders    = numericProperties<int,    T>();
            var floatAdders  = numericProperties<float,  T>();
            var shortAdders  = numericProperties<short,  T>();
            var doubleAdders = numericProperties<double, T>();
    
            foreach (var item in items)
            {
                sumValueTo(intAdders,    item);
                sumValueTo(floatAdders,  item);
                sumValueTo(shortAdders,  item);
                sumValueTo(doubleAdders, item);
            }
    
            T result = new();
    
            assignResults(intAdders,    result);
            assignResults(floatAdders,  result);
            assignResults(shortAdders,  result);
            assignResults(doubleAdders, result);
    
            return result;
        }
    
        static void sumValueTo<TNumber, T>(List<NumericPropertyAdder<TNumber>> numericProperties, T item) where TNumber : INumber<TNumber>
        {
            foreach (var numericProperty in numericProperties)
            {
                numericProperty.Add(item!);
            }
        }
    
        static void assignResults<TNumber, T>(List<NumericPropertyAdder<TNumber>> numericProperties, T item) where TNumber : INumber<TNumber>
        {
            foreach (var numericProperty in numericProperties)
            {
                numericProperty.AssignResult(item!);
            }
        }
    
        static List<NumericPropertyAdder<TNumber>> numericProperties<TNumber, TOwner>() where TNumber : INumber<TNumber>
        {
            return (
                from   prop in typeof(TOwner).GetProperties()
                where  prop.PropertyType.IsAssignableTo(typeof(INumber<TNumber>))
                select new NumericPropertyAdder<TNumber>(prop)
            ).ToList();
        }
    }
    
    public sealed class NumericPropertyAdder<T> where T: INumber<T>             
    {
        public NumericPropertyAdder(PropertyInfo property)
        {
            _property = property;
        }
    
        public void Add(object propertyHolder)
        {
            var value = (T) _property.GetValue(propertyHolder)!;
            _sum += value;
        }
    
        public T Sum()
        {
            return _sum;
        }
    
        public void AssignResult(object propertyHolder)
        {
            _property.SetValue(propertyHolder, _sum);
        }
    
        T _sum = T.AdditiveIdentity;
    
        readonly PropertyInfo _property;
    }
    

    My conclusion is that the added complexity of handling the generic types like this might not actually be worth it... ;)