2

I'm given classes ContainerA, ContainerB, ElementA and ElementB. I can't modify these classes because of very-good-reasons™ with the exception that I can add interfaces.

What I would like is something like the IElements interface (expect legal) as it would allow me to easily loop over the elements and read properties from the elements of either ContainerA or ContainerB without knowing which concrete implementation is actually used. There are no methods on any of the classes. For the element classes I only need to access properties implemented by the ElementA, exemplified by the Id property.

What are my options?

public interface IElements
{
    List<ElementA> Elements { get; set; }
}

public class ContainerA : IElements
{
    public List<ElementA> Elements { get; set; }
}

public class ContainerB : IElements
{
    public List<ElementB> Elements { get; set; }
}

public class ElementA
{
    public string Id { get; set; }    
}

public class ElementB : ElementA
{
}

I hope to achieve something similar to:

IElements container = ...;
foreach (var element in container.Elements)
{
    Console.WriteLine(element.Id);
}
Linus
  • 3,254
  • 4
  • 22
  • 36
  • If ElementA and ElementB both implement an IElement interface (or have a common base class), `IElements` could have a `List` (or a `List`). You could look at something like giving `IElements` a non-generic `IEnumerable` `Elements`, and implementing it explicitly in `ContainerA` and `ContainerB`. – 15ee8f99-57ff-4f92-890c-b56153 Jun 03 '19 at 14:14
  • Do both CointainerA and B implement the same methods with the same names ? – JBdev Jun 03 '19 at 14:15
  • 2
    Generics (with constraints) might also work for you here, but with no context, it's hard to suggest the correct strategy. – DavidG Jun 03 '19 at 14:16
  • What are you hoping to do with this universal collection type? Add `ElementA` instances to a collection of `ElementB`? Or call methods of `ElementA` on an instance of `ElementB` without knowing which type it is? – 15ee8f99-57ff-4f92-890c-b56153 Jun 03 '19 at 14:19
  • Updated the question. There are no methods at all, only properties. I only need to access properties from the base class `ElementA`. – Linus Jun 03 '19 at 14:23
  • so expose the properties thru the interface....IElements would be Int, long, the actual properties which are on both not another list. – Seabizkit Jun 03 '19 at 14:26

2 Answers2

1

There are many ways to slice this cake, but I would consider using generics with constraints here. I'd also consider an interface (or an abstract base class) for the element object, so for example:

public interface IElement
{
    int Id { get; set; }
}

And your generic elements interface would look like this:

public interface IElements<TElement>
    where TElement : ElementA
{
    List<TElement> Elements { get; set; }
}

Making your container classes look something like this:

public class ContainerA : IElements<ElementA>
{
    public List<ElementA> Elements { get; set; }
}

public class ContainerB : IElements<ElementB>
{
    public List<ElementB> Elements { get; set; }
}

Now you can access the Id property of any object that implements your interfaces, for example:

public void DoFoo<TElement>(IElements<TElement> elements)
    where TElement : ElementA
{
    foreach (var element in elements.Elements)
    {
        // This will compile fine
        var id = element.Id;
    }
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • This is probably what I'll end up doing. However, I would prefer a non-generic `IElements` since I know the elements are always assignable to `ElementA` and that's all I need. – Linus Jun 03 '19 at 14:46
1

IEnumerable is covariant, i.e., an IEnumerable<SubtypeOfT> is a subtype of IEnumerable<T>. Thus, the following should work:

public interface IElements
{
    IEnumerable<ElementA> ReadOnlyElements { get; }
}

public class ContainerA : IElements
{
    public List<ElementA> Elements { get; set; }
    IEnumerable<ElementA> IElements.ReadOnlyElements => Elements;
}

public class ContainerB : IElements
{
    public List<ElementB> Elements { get; set; }
    IEnumerable<ElementA> IElements.ReadOnlyElements => Elements;
}

You can iterate through them as follows:

IElements container = ...;
foreach (var element in container.ReadOnlyElements)
{
    Console.WriteLine(element.Id);
}
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • This is a reasonable solution but unfortunately the "can only add interfaces" in this case actually means that I can't actually add the implementation of the interface, the interface can only use stuff that's already there. – Linus Jun 03 '19 at 14:39
  • @Linus: Ah, too bad. You cannot even add explicit interface implementations? They won't change the public interface of your classes... – Heinzi Jun 03 '19 at 14:42
  • 1
    You'd need return type covariance to define `IEnumerable Elements { get; }` in the interface. It would be type-safe, but unfortunately, [C# does not support that](https://stackoverflow.com/q/5709034/87698). – Heinzi Jun 03 '19 at 14:50
  • @Linus: Oh, and if the very-good-reasons™ are that the code is automatically generated, I'd like to point out that [partial classes](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods) can be used to easily add class members to an auto-generated class. – Heinzi Jun 03 '19 at 18:47