0

Ok, I know that I might have strayed to the realm of overcomplicating things, but I'd still like to know how to make following work. I'm going to rework actual application to don't go to such lengths, but for future reference this seems like a good question.

The situation:

I have an ExpandoObject to which I add properties by string paramName. It all worked great, but then I realized that sometimes I want to have an ExpandoObject property that is some kind of IEnumerable<>, and when I set param for 2nd and subsequent times, I don't want to change the stored list, but I want to concatenate it.

The problem is, of course, that IEnumerable doesn't have Add method. So, I can just replace with a concatenation of two IEnumerable<>'s of the same type. I can sacrifice the ability to use non-generic IEnumerables.

I felt very ingenious until the IntelliSense disallowed me to write eo[paramName] = (eo[paramName] as IEnumerable<>).Concat(param as IEnumerable<>); As you can see from full code, I know for sure, that both param and eo[paramName] are some kind of IEnumerable<>, but I don't know how to tell compiler what I want him to do.

Any suggestions?

private void SetParam<T>(IDictionary<string, object> eo, string paramName, T param){
    // eo is (ExpandoObject as IDictionary<string, object>)
    var enumerableType = GetEnumerableType(typeof (T));
        if (enumerableType == null)
        {
            // is not IEnumerable<>, but it might be not-supported IEnumerable
            if (typeof (T).GetInterfaces().Contains(typeof (System.Collections.IEnumerable)))
                throw new NotSupportedException("Non-generic collection types are not supported");

            // it is just plain not-colection, so set value
            eo[paramName] = param;
        }
        else
        {
            // first ensure that there is at least empty collection
            if (eo[paramName] == null)
                eo[paramName] = Activator.CreateInstance<T>();

            // and concatenate
            eo[paramName] = (eo[paramName] as IEnumerable<>).Concat(param as IEnumerable<>);
           // or whatever other way to add param to eo[paramName]
        }
    } 
    // from: http://stackoverflow.com/a/1846690/1417905
    private static Type GetEnumerableType(Type type)
    {
        return (from intType in type.GetInterfaces()
            where intType.IsGenericType && intType.GetGenericTypeDefinition() == typeof (IEnumerable<>)
            select intType.GetGenericArguments()[0]).FirstOrDefault();
    }
Gerino
  • 1,943
  • 1
  • 16
  • 21
  • I don't get it. Do you have a class named ExpandoObject? Or do you just want to populate the Dictionary? – EagleBeak Jan 27 '15 at 14:16
  • @EagleBeak ExpandoObject is Microsoft dynamic class: https://msdn.microsoft.com/en-us/library/system.dynamic.expandoobject%28v=vs.110%29.aspx :) – Gerino Jan 27 '15 at 14:41
  • How about using Linq: `var result = ((IEnumerable)prm).OfType().Union((IEnumerable)prm2).OfType()` – Sten Petrov Jan 27 '15 at 14:56

2 Answers2

1

You can force it using reflection. I declared a method like this:

public IEnumerable<T> Concat<T>(IEnumerable<T> obj, IEnumerable<T> obj2)
{
    return obj.Concat(obj2);
}

And then instead of the line eo[paramName] = (eo[paramName] as IEnumerable<>).Concat(param as IEnumerable<>);

insert this code:

MethodInfo info = GetType().GetMethod("Concat");
info = info.MakeGenericMethod(enumerableType);
eo[paramName] = info.Invoke(this, new [] {eo[paramName], param});
Andrej Kmetík
  • 158
  • 3
  • 10
-1

You have to specify the type parameter for IEnumerable<T>. And you aren't allowed to concat two enumerables of different type. For example this is not allowed: As Servy mentioned in the comments, it IS allowed to concat two enumerables of different type. Even if they don't explicitely share a base type, because every reference type in .NET is of type object. So it is allowed to:

IEnumerable<Apple> apples = new List<Apple>();
IEnumerable<Car> cars = new List<Car>();

cars.Concat(apples); // compiler error
IEnumerable<object> enuerableOfObjets = cars.Concat<object>(apples);

But in my opinion this leads the whole concept of generic collections ad absurdum.

You can accomplish nearly what you want by splitting the method in two methods:

    private void SetParam<T>(IDictionary<string, object> eo, string paramName, T param)
    {
        // is not IEnumerable<>, but it might be not-supported IEnumerable
        if (typeof(T).GetInterfaces().Contains(typeof(System.Collections.IEnumerable)))
            throw new NotSupportedException("Non-generic collection types are not supported");

        // it is just plain not-colection, so set value
        eo[paramName] = param;
    }

    private void SetParam<T>(IDictionary<string, object> eo, string paramName, IEnumerable<T> param)
    {
        if (!eo.ContainsKey(paramName))
            eo[paramName] = param;
        else
            eo[paramName] = (eo[paramName] as IEnumerable<T>).Concat(param);
    }

This solution concats IEnumerable<T>'s with the same type T. If the existing enumerable has an different type to the one you are trying to add, it will result in a runtime error.

Thomas Lielacher
  • 1,037
  • 9
  • 20
  • I'll test it later - I think I might need to actually _not_ use overloading `SetParam`, but do a manual check and call either `SetParam` or `SetEnumerableParam`, as to runtime both overloads will fit, when called: `U myParam; SetParam(eo, paramName, myParam);` – Gerino Jan 27 '15 at 14:44
  • Also - in no way would that method can called for `IEnumerable` and `IEnumerable` using the same paramName, other part of code guarantees this for me :) – Gerino Jan 27 '15 at 14:46
  • I don't get what you mean by "as to runtime both overloads will fit" – Thomas Lielacher Jan 27 '15 at 14:49
  • 1
    `And you aren't allowed to concat two enumerables of different type` Yes you are. `IEnumerable` is covariant. You can perform the operation so long as there is a common ancestor type, which there will be (`object`) as long as the types are reference types. – Servy Jan 27 '15 at 15:20
  • @Servy I am aware of that. Thus I chose `Apple` and `Car` for my example. If I had chosen e.g. `Apple` and `Pear` someone could argue, that they could have a common base type. And of course, all classes share the same base type `object` and you could use `IEnumerable`, but this leads the whole concept of generics ad absurdum IMHO. – Thomas Lielacher Jan 28 '15 at 06:42
  • @ThomasLielacher As you said, `Apple` and `Car` share a common base type, `object`. You *can* `Concat` them together into an `IEnumerable`. That sequence may or may not be useful, but you absolutely can do it. Saying that you can't is just flat wrong. – Servy Jan 28 '15 at 14:58