Home > Net >  Concatenating Lists at runtime
Concatenating Lists at runtime

Time:07-09

I am handling classes that wrap collections. For example:

public class CollA 
{
  public List<SomeType> Items {get;set;}

  // other properties I'm not interested in
}

I am guaranteed that the collection classes will have exactly ONE property that is of List<T>

Now, I find myself with a requirement such that I may have many instances of CollA and I am asked to return a new instance of CollA where the property Items contains a union of the Items properties of the individual CollA instances. So, for example:

var A = new CollA(Items = new List<SomeType> 
{
  new SomeType("A"), new SomeType("B")
};
var B = new CollA(Items = new List<SomeType> 
{
  new SomeType("C"), new SomeType("D")
};

var result = SomeMythicalCombine(A, B);

// result.Items == { new SomeType("A"), new SomeType("B"), new SomeType("C"), new SomeType("D") }

This, if the types are all known at compile time is easy, but I need to do it with the types not being known until runtime.

I've got part of the way, I think, using reflection....

public T SomeMythicalCombine (params object[] collections)
{
    var collectionType = typeof(T);
    
    var listProperty = collectionType.GetProperties()
        .Single(p=> typeof(IList).IsAssignableFrom(p.PropertyType));

    var listPropertyName = listProperty.Name;

    var result = Activator.CreateInstance(collectionType);

    var innerType = listProperty.PropertyType.GenericTypeArguments[0];
    var listType = typeof(List<>).MakeGenericType(innerType);
    var list = Activator.CreateInstance(listType);
    
    foreach(var collection in collections)
    {
        var listValues = collection.GetType().GetProperty(listPropertyName).GetValue(collection);
        
        // listItems is an object here and I need to find a way of casting it
        // to something I can iterate over so I can call (list as IList).Add(something)
    }   
    
    // Then, I think, all I need to do is set the appropriate property on the 
    // the result item
    result.GetType().GetProperty(listPropertyName).SetValue(result, list);
    
    return result as T;
}

Can anyone fill in the gap in my thinking, please?

CodePudding user response:

So basically if you know the type at compile time, you can do this:

    var result = new CollA { Items = new[] { A, B }.SelectMany(c => c.Items).ToList() };

If you can require all your collection wrappers to implement an interface, it should be pretty simple to extract this into a generic method.

public interface ICollectionWrapper<T> { List<T> Items { get; set; } }

T SomeMythicalCombine<T, T2>(params T[] wrappers) where T : ICollectionWrapper<T2>, new()
{
    return new T() { Items = wrappers.SelectMany(w => w.Items).ToList() };
}

That presupposes you can call the method with the right generic parameter. If your calling code knows the types of the collections you're dealing with, you can do this:

var result = SomeMythicalCombine(A, B);

But honestly if your calling code knows that, you might be better off using the first code snippet: it's concise and clear enough. Assuming you literally have a collection of objects that you just happen to know will all have the same run-time type, you should be able to use a little reflection to get that type and invoke the helper method with the right generic parameters. It's not ideal, but it might be faster/simpler than writing the entire method to work using reflection.

CodePudding user response:

you can do this : var combined = A.Items.Concat(B.Items).

However, if the property is a part of interface or base class implementation, then you can target the implementation instead something like this :

public IList<TResult> SomeMythicalCombine<TResult>(params IInterface[] collection) // use interface or base class 
{
  // assuming that all collection would have the same element type.
}

if it is not a part of other implementations, then you can implement an interface and apply it to all classes, this would be an insurance that this collection will always be there as long as the class implements the interface.

if it's hard to achieve that, then you can and you see that reflection is your best option, you can use something like this :

// assuming all collections have the same property of type List<TResult> type.
// if they're different, then return an object instead. and change List<TResult> to IList 
public IEnumerable<TResult> CombineLists<T, TResult>(params T[] instances) 
    where T : class
{
    if (instances?.Any() == false) yield break;

    foreach(var obj in instances)
    {
        if (obj == null) continue;

        var list = obj.GetType()
            .GetProperties()
            .FirstOrDefault(p => typeof(List<TResult>).IsAssignableFrom(p.PropertyType))
            ?.GetValue(obj) as List<TResult>;

        if (list?.Count == 0) continue;
        
        foreach (var item in list)
            yield return item;
    }
}

usage :

var combined = CombineLists<CollA, string>(A, B);
  • Related