Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Better way to convert IEnumerable<T> to user type

Tags:

c#

.net

linq

I have a custom collection type, defined as such:

public abstract class RigCollectionBase<T> : Collection<T>, IEnumerable<T>, INotifyPropertyChanged, IBindingList, ICancelAddNew where T : BusinessObjectBase, new()

Note: this is the base class, there are 20 or so child classes that are implemented like so:

public class MyCollection : RigCollectionBase<MyObject>

We use a lot of Linq in our code, and as you probably know, Linq functions return IEnumerable<T>. What I'm looking for, is an easy and simple way to go back to MyCollection from IEumberable<MyObject>. Casting is not allowed, I get the exception "Cannot cast from ..."

Here is the answer I came up with, and it does work, but it seems kind of clunky and...overcomplicated. Maybe its not, but I figured I would get this out there to see if there's a better way.

public static class Extension
{
    /// <summary>
    /// Turn your IEnumerable into a RigCollection
    /// </summary>
    /// <typeparam name="T">The Collection type</typeparam>
    /// <typeparam name="U">The Type of the object in the collection</typeparam>
    /// <param name="col"></param>
    /// <returns></returns>
    public static T MakeRigCollection<T, U> (this IEnumerable<U> col) where T : RigCollectionBase<U>, new() where U : BusinessObjectBase, new()
    {
        T retCol = new T();

        foreach (U myObj in col)
            retCol.Add(myObj);

        return retCol;
    }
}

What I'm really looking for, I guess, is this. Is there a way to implement the base class so that I can use a simple cast to go from IEnumerable into MyCollection...

var LinqResult = oldCol.Where(a=> someCondition);
MyCollection newCol = (MyCollection)LinqResult;

No, the above code doesn't work, and I'm actually not 100% certain why that is...but it doesn't. It just feels like there is some very obvious step that I'm not seeing....

like image 901
Nevyn Avatar asked Apr 03 '13 12:04

Nevyn


5 Answers

Your method MakeRigCollection is basically the right way to do it. Here is a variant that is slightly more verbose to use but much simpler to implement:

TCollection MakeRigCollectionSimple<TCollection, TItem>(
    this IEnumerable<TItem> items, TCollection collection)
    where TCollection : ICollection<TItem>
{
        foreach (var myObj in items)
            collection.Add(myObj);
        return collection;
}

I hope I got it right. You use it like this:

MakeRigCollectionSimple(items, new MyCollection());

or

items.MakeRigCollectionSimple(new MyCollection());

Now you have the 2nd argument to fill out, but in exchange we were able to get rid of all the crazy generics stuff. Just simple generics left. And type inference kicks in fully. Also, this will work for all collection types, not just your RigCollections.

like image 121
usr Avatar answered Nov 12 '22 23:11

usr


While your collection does implement IEnumerable<T>, you can't cast a simple IEnumerable<T> to your specific collection because of how inheritance works.

That's why there are built-in LINQ extension methods such as IEnumerable<T>.ToList(), that does exactly what you wrote.

The only difference is that List<T> provides a public constructor that takes IEnumerable<T> as parameter.

like image 36
ken2k Avatar answered Nov 12 '22 23:11

ken2k


Add a constructor to MyCollection that accepts an IEnumerable<T>, and do:

var newCol = new MyCollection(oldCol.Where(a=> someCondition));
like image 1
Willem Avatar answered Nov 12 '22 22:11

Willem


You can't cast, because the LINQ methods don't change your original collection. They return new ones. The new ones aren't instances of your collection but of LINQ-specific collections, depending on which method you used. The reason for this is the deferred nature of the LINQ methods.

You have several possibilities:

  1. You could create an explicit or implicit cast operator in your class but you would need to implement it in every one of your child classes.
  2. You can create a constructor that takes an IEnumerable<T> and directly initialize your collection from it - similar to the constructor on List<T>.
like image 1
Daniel Hilgarth Avatar answered Nov 12 '22 22:11

Daniel Hilgarth


This is a little bit complicated, but in general you can imagine, that LINQ enumerates through your collection (no matter of the type, it is just important, that it can be converted into IQueryable) and adds all matching object references to a new list. It depends on the IQueryProvider which container object is used to collect the query results. So the most obvious option is to create an custom IQueryProvider. Everybody who tried this, knows how much pain this can be...

However IQueryProvider typically returns an IEnumerable, which leads to two options:

  1. Use LINQ's ToList extension method, to create an System.Collections.Generic.List and continue using simple lists as containers, or
  2. Add an constructor, accepting an IEnumerable.

Both ways are much more similar, than you might think, because ToList is implemented somehow like this:

public static List<T> ToList<T>(this IEnumerable<T> enumerable)
{
    return new List<T>(enumerable);
}

So it simply delegates all the hard work to the respective constructor of List<T>.

Most collections are supporting construction by IEnumerable parameters. I don't actually know why System.Collections.ObjectModel.Collection<T> doesn't support IEnumerable, but IList. However this gives you the possibility to implement your collection base class with two additional constructors to simplify your queries' results:

MyRigCollectionBase(IEnumerable<T> enumerable)
    : this (enumerable.ToList())
{
    // Delegates to constructor below
}

MyRigCollectionBase(IList<T> list)
    : base (list)
{
    // Delegates to constructor of Collection<T>.
}

This does not really solve your overload, but it gives you the chance to build up custom collections from queries:

var coll = new MyCollection(oldColl.Where(x => x.AddToList));

And it give's clients the possibility to use different ways to manage their query results.

Also this enables you to provide custom extensions: 1

public class MyCollection : MyRigCollectionBase<MyObject>
{
    public static ToMyCollection(this IEnumerable<MyObject> enumerable)
    {
        return new MyCollection(enumerable);   // Calls our previously declared constructor.
    }
}

Now you can query like this:

var coll = oldColl.Where(x => x.AddToList).ToMyCollection();

Each specialization of your collection could define it's own extension within the scope of it's declaration. Each query that returns an IEnumerable<MyObject> would be able to convert it's result into MyCollection.

1I am not 100% sure if this IEnumerable<MyObject> works as an parameter, and the extension can get called for an query returning IEnumerable<MyObject>. Some confirmation would be nice!

like image 1
Carsten Avatar answered Nov 12 '22 23:11

Carsten