3

I'm seeing a very strange problem when overriding an abstract method and trying to call the base class's method that I'm currently overriding.

//In Dll "A"
namespace Rhino.Etl.Core.Operations
{
    using System;
    using System.Collections;
    using System.Collections.Generic;

    public class Row {}

    public interface IOperation
    {
        IEnumerable<Row> Execute(IEnumerable<Row> rows);
    }

    public abstract class AbstractOperation : IOperation
    {
        public abstract IEnumerable<Row> Execute(IEnumerable<Row> rows);
    }

    public abstract class AbstractDatabaseOperation : AbstractOperation
    {
    }

    public abstract class SqlBulkInsertOperation : AbstractDatabaseOperation
    {
        public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
        {
            Console.WriteLine("SqlBulkInsertOperation");
            return rows;
        }
    }
}

//In console app "B"
namespace MyStuff
{
    using System;
    using System.Collections;
    using System.Collections.Generic;

    class Program
    {
        static void Main(string[] args)
        {
            ActualEtlOperation e = new ActualEtlOperation();
            e.Execute(new Row[0]);

            Console.ReadLine();
        }
    }

    public abstract class SqlBulkInsertWithTruncateOperation : SqlBulkInsertOperation
    {
        public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
        {
            Console.WriteLine("Truncate");
            base.Execute(rows);
            return rows;
        }
    }

    public class ActualEtlOperation : SqlBulkInsertWithTruncateOperation
    {

    }
}

Basically, the SqlBulkInsertOperation does not do what I need it to do, so I need to do a little work before and after I call Execute(rows) on it by overriding it. But the code in SqlBulkInsertOperation.Execute(Rows) is not executed.

When running this code in the debugger in Visual Studio the debugger the code's not executed. When I hover the mouse over "base" in the Visual Studio editor it knows the base class is of type SqlBulkInsertOperation.

What am I missing?

Tim Gebhardt
  • 405
  • 5
  • 9
  • Very suspicious. This is very simple C# inheritance behavior, and the code you posted, when placed in an isolated test, performs as expected. There must be something in your actual production code that differs significantly. – KeithS Sep 29 '10 at 20:00
  • After seeing @Alex Lo's comment about the disconnected middle I should probably add that the BaseDatabaseOperation and SqlBulkInsertOperation are not part of my own project. They're a 3rd party library (from RhinoETL). It's also why I couldn't just copy/paste the code that's not working correctly. Sorry about that. – Tim Gebhardt Sep 29 '10 at 20:33
  • 1
    @Tim, if you don't have the code for SqlBulkInsertOperation, how are you determining that the base code is not being executed? If it's purely by side effects, it may be executing, but not doing what you think it should. Try debugging and stepping into the disassembled code to verify that the callstack really is stepping into the base method. – Dan Bryant Sep 29 '10 at 20:43
  • what is the output of the current example, "Truncate", but not "SqlBulkInsertOperation"? dramatically changing the question makes all the prior remarks/questions confusing. – Alex Lo Sep 29 '10 at 21:28
  • @Alex, sorry for the confusion. Yes, if you compiled this app in a new solution in VS you get the correct output which is "Truncate" then "SqlBulkInsertOperation". However, in my actual program using the actual RhinoETL library "SqlBulkInsertOperation" would not be printed. I can verify this because I can change the "base.Execute(rows);" line to "base.Execute(null);", which should cause the SqlBulkInsertOperation method implemented in RhinoETL to throw an exception but it does not. – Tim Gebhardt Sep 30 '10 at 14:21
  • 1
    OK, well then none of us can really tell what is going on because obviously something about the set up is being missed. My suggestion would be to scrap the reuse via inheritance and go with composition. Have your class inherit only from IOperation and have it instantiate the SqlBulkInsertOperation and call Execute directly on that, then there should be no ambiguity. – Alex Lo Sep 30 '10 at 16:39
  • @Alex: I agree with you that the deep inheritance tree isn't the best way to handle this, but it's the way the 3rd party library is intended to be used :(. – Tim Gebhardt Sep 30 '10 at 17:15
  • @Alex: I created a sample application that reproduces the problem. See @Jon's answer and the comments for the thread if you care to take a look. – Tim Gebhardt Sep 30 '10 at 18:03

5 Answers5

10

EDIT: I've found the problem... and ironically, my "psychic debugging" comment to Eric wasn't so far off, given this blog post. Here's a short but complete program which will demonstrate what's going on...

using System;
using System.Collections.Generic;

public class Row {}

public abstract class BaseDatabaseOperation
{
    public abstract IEnumerable<Row> Execute(IEnumerable<Row> rows);
}

public abstract class SqlBulkInsertOperation : BaseDatabaseOperation
{
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
    {
        Console.WriteLine("In SqlBulkInsertOperation.Execute");
        foreach (var row in rows)
        {
            yield return row;
        }
    }
}

public class MyOverride : SqlBulkInsertOperation
{
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
    {
        Console.WriteLine("In MyOverride.Execute");
        return base.Execute(rows);
    }
}

class Test
{
    static void Main()
    {
        BaseDatabaseOperation x = new MyOverride();
        x.Execute(new Row[0]);
    }
}

That will print "In MyOverride.Execute" but it *won't" print "In SqlBulkInsertOperation.Execute"... because that method is implemented with an iterator block.

Of course, you can demonstrate this much more simply:

using System;
using System.Collections.Generic;

class Test
{
    static IEnumerable<string> Foo()
    {
        Console.WriteLine("I won't get printed");
        yield break;
    }

    static void Main()
    {
        Foo();
    }
}

Nothing is using the return value of the method - it's being passed back to Main, but nothing ever calls GetEnumerator() on it, and then MoveNext() on the result... so the body of the method is never executed.

See my article on iterator blocks, part two of Eric's blog post, or download chapter 6 from the home page of the first edition of C# in Depth (chapter 6 covers iterator blocks, and is free) for more details on this.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • You're right. I've edited the code above to compile and more closely reflect the situation. If you compile the code in VS.NET (into 2 assemblies as specified in the comments) then everything works as I would expect it to: print the 2 lines. The 3rd party library I'm using for the SqlBulkInsertOperation is an open source library, so instead of adding a reference to the DLL, I've added the project to my solution. When debugging and when I get to the base.Execute(rows) call I try to step into the method and it just skips over that line. – Tim Gebhardt Sep 30 '10 at 14:31
  • @Tim: That sounds like a debugger issue rather than it actually not doing things. Can you debug into the Rhino code for other calls? – Jon Skeet Sep 30 '10 at 14:43
  • @Jon: I can debug other components of the RhinoETL dll. The other way I can prove that it's not actually executing the call is I can change base.Execute(rows) to base.Execute(null). If I do that the program should throw an exception (SqlBulkInsertOperation.Execute() checks the Rows argument for null and throws an Exception if it is), but it does not. My program exits like normal, and even if I tell VS.NET to break on all exceptions it never breaks. – Tim Gebhardt Sep 30 '10 at 14:58
  • @Tim: Can you reproduce this in a separate project which would be easy for us to check? It still sounds very odd... you are *definitely* using the right SqlBulkInsertOperation class, rather than another one which happens to be in the same namespace? If you hover over the name (where you're specifying that your class derives from it) and right-click/"Go to definition" does it go to the right place? – Jon Skeet Sep 30 '10 at 15:02
  • @Jon: If I hover over the "base" part of the line it says it's the class I'm looking for (Rhino.Etl.Core.Operations.SqlBulkInsertOperation). Right-clicking the method and navigating to the declaration does indeed go to the method declaration I'm looking for (and indeed the first line of that method is checking the argument for null, to help me prove that it should be throwing an exception). I don't think it's a debugger issue: I get no error with base.Execute(null) running the program from the command line, but I do if I add a "throw Exception("")" above base.Execute(). – Tim Gebhardt Sep 30 '10 at 15:23
  • @Jon: Aha! I've got a sample project using the actual RhinoETL to reproduce the problem. If anyone's interested it can be downloaded from http://www.gebhardtcomputing.com/SampleWeird.zip. The ConsoleApplication project is easy enough to audit (it's 1 file), but if you're not willing to run the RhinoETL code (if you think I'm a shady character), you can just grab it from the source via Git at http://github.com/ayende/rhino-etl. You don't need a separate DB to run the project. In fact, if you can get a DB not found error, then my problem would be solved! – Tim Gebhardt Sep 30 '10 at 16:01
  • @Tim: Windows is claiming that your zip file is corrupt - could you take a look? – Jon Skeet Sep 30 '10 at 16:05
  • @Jon: Try again. http://www.gebhardtcomputing.com/SampleWeird.zip. The first time I created the zip with 7zip. I've re-uploaded the ZIP after making it with the Windows shell. – Tim Gebhardt Sep 30 '10 at 16:09
  • @Tim: Nope, still no joy. Is it possible you're using FTP in ASCII mode? – Jon Skeet Sep 30 '10 at 16:20
  • @Jon: try this once again: http://www.gebhardtcomputing.com/SampleWeird.zip?attredirects=0 I had a friend try to download and open it successfully before I posted this comment. – Tim Gebhardt Sep 30 '10 at 16:49
  • @Tim: Got it now - the other ones were only 2K instead of over a meg :) – Jon Skeet Sep 30 '10 at 16:56
  • @Tim: Have reproduced the problem, and will investigate further tonight. – Jon Skeet Sep 30 '10 at 17:05
  • @Jon: Great! Thanks for all the help. The stripped down example makes this even more maddening as it gets rid of all the other things going on in the original program which might have been the cause (some bad configuration, etc.). – Tim Gebhardt Sep 30 '10 at 17:17
  • @Tim: I've worked it out - see my (completely edited) answer. I should have spotted that the method returned `IEnumerable`... try *using* the result of the method, and all will work properly :) – Jon Skeet Sep 30 '10 at 18:47
  • @Jon: Thanks for taking the time to help with this. Your anonymous internet friend Tim really appreciates it! – Tim Gebhardt Sep 30 '10 at 19:09
6

Here's the code I ran:

using System;
using System.Collections.Generic;
public class Row {}
public abstract class BaseDatabaseOperation 
{ 
    public abstract IEnumerable<Row> Execute(IEnumerable<Row> rows); 
} 

public abstract class SqlBulkInsertOperation : BaseDatabaseOperation 
{ 
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows) 
    { 
        Console.WriteLine("base");
        return null;
    } 
} 

public class MyOverride : SqlBulkInsertOperation 
{ 
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows) 
    { 
        Console.WriteLine("override");
        base.Execute(rows);
        return null;
    } 
} 

static class P 
{
    static void Main()
    {
        var m = new MyOverride();
        m.Execute(null);
    }
}

It runs fine; it produces "override" and "base".

Tell you what: modify the program I've posted here so that it produces the problem you are experiencing, and we'll analyze that. It is very difficult to analyze a problem when you cannot actually see the real code that is having problems.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 3
    Are your psychic debugging skills having an off day then? – Jon Skeet Sep 29 '10 at 20:06
  • It turns out the "psychic debugging" comment was an accidental clue - as was the fact that the return type is `IEnumerable`. Guess what the actual implementation of SqlBulkInsertOperation.Execute uses :) – Jon Skeet Sep 30 '10 at 18:48
5

Works fine for me. Your code needs a little cleanup - are you sure you're compiling and executing this code, and not debugging an older build of your code?

class Program
{
    public class Row { }

    public abstract class BaseDatabaseOperation
    {
        public abstract IEnumerable<Row> Execute(IEnumerable<Row> rows);
    }

    public abstract class SqlBulkInsertOperation : BaseDatabaseOperation
    {
        public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
        {
            Console.WriteLine("done");
            return rows;
        }
    }

    public class MyOverride : SqlBulkInsertOperation
    {
        public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
        {
            return base.Execute(rows);
        }
    }

    static void Main(string[] args)
    {
        var x = new MyOverride();
        x.Execute(null);
    }
}
dthorpe
  • 35,318
  • 5
  • 75
  • 119
3

Your example does not compile, some returns are missing, but the following modified example

public class Row
{
}

public abstract class BaseDatabaseOperation
{
    public abstract IEnumerable<Row> Execute(IEnumerable<Row> rows);
}

public abstract class SqlBulkInsertOperation : BaseDatabaseOperation
{
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
    {
        Console.WriteLine("does useful work");
        return new Row[0];
    }
}

public class MyOverride : SqlBulkInsertOperation
{
    public override IEnumerable<Row> Execute(IEnumerable<Row> rows)
{

        Console.WriteLine("do own work here");

    //This does not execute code in base class!
        base.Execute(rows);

        return new Row[0];
}
}

Called like this:

MyOverride mo = new MyOverride();
mo.Execute(new Row[0]);

generates the output

do own work here
does useful work

You must have done something else not included in your example.

Albin Sunnanbo
  • 46,430
  • 8
  • 69
  • 108
3

are these all being compiled at the same time? you might be suffering from the disconnected middle base class http://blogs.msdn.com/b/ericlippert/archive/2010/03/29/putting-a-base-in-the-middle.aspx

Alex Lo
  • 1,299
  • 8
  • 10
  • I don't think so - because without the middle class, the compiler would know there's no concrete base method to call. (Try making MyOverride derive directly from BaseDatabaseOperation.) – Jon Skeet Sep 29 '10 at 20:07
  • the middle class may be stale, i think that's the best explination at the moment – Alex Lo Sep 29 '10 at 20:20