Home > other >  Why is the derived class' virtual override only called when it's not written as an iterato
Why is the derived class' virtual override only called when it's not written as an iterato

Time:11-02

I'm adding unit tests for a class that uses Entity Framework. I've added test doubles based on this, and got a few tests running successfully. However, I reached a stumbling block when I began implementing a test targeting a section of code that relies on DbSet<T>.RemoveRange(IEnumerable<T>). I added the following override to TestDbSet<T> (derived from DbSet<T>; from the link):

public override IEnumerable<TEntity> RemoveRange(IEnumerable<TEntity> entities)
{
    foreach (var item in entities)
    {
        Data.Remove(item);
        yield return item;
    }
}

However, when I have an instance of TestDbSet<T> assigned to a DbSet<T> variable and try to call RemoveRange, it doesn't run the override that I wrote above -- it seems to be using the base class' implementation.

This baffled me for a bit. I confirmed that the instance was in fact a TestDbSet<T>, and of course it was, because the other method overrides were working. So I tried to think what was different about this method, and well, it's the only one iterator method (yield return), so I rewrote it without that:

public override IEnumerable<TEntity> RemoveRange(IEnumerable<TEntity> entities)
{
    entities = entities.ToList();
    foreach (var item in entities)
    {
        Data.Remove(item);
    }
    return entities;
}

...and suddenly it worked? Now when I call RemoveRange on the DbSet<T> (that's actually a TestDbSet<T>), it calls TestDbSet<T>.RemoveRange instead of DbSet<T>.RemoveRange.

I've tried switching it back and forth. I've confirmed that the ToList() alone doesn't fix it. Only avoiding yield return appears to fix it.

Can someone explain what's causing this? Does using an iterator method change the method's declaration or something? If so, how is that allowed/compatible with override?

CodePudding user response:

Most likely you do not enumerate the return value of RemoveRange. Iterator methods (the ones with yield return/yield break) are special - they are "lazy" and your code inside them is NOT executed right when you call the method, but only when the returned value is being enumerated. For example:

public static void Main() {
    Test();
}

static IEnumerable<int> Test() {
    Console.WriteLine("Got in");
    yield return 1;
    Console.WriteLine("Returned 1");
}

This will print nothing to the console, because the return value of Test is never enumerated. This on the other hand:

public static void Main() {
    Test().ToList();
}

static IEnumerable<int> Test() {
    Console.WriteLine("Got in");
    yield return 1;
    Console.WriteLine("Returned 1");
}

Will print both statements, because we are enumerating the result with ToList().

This leads us to the fact you can't use iterator methods in your case - RemoveRange is not a valid candidate for that. Yes it returns IEnumerable, but the main purpose of this function is to remove entities, while the return value is kind of optional side effect and you can't expect the caller to always enumerate it (in most cases the return value will be completely ignored).

  • Related