Custom ADO.NET Provider, question about DbCommand.ExecuteReader

  • Thread starter Thread starter Jed Ozone
  • Start date Start date
J

Jed Ozone

I'm converting over a ADO.NET provider I wrote in 1.1 .NET to 2.0. I don't
understand why the DbCommand.ExecuteReader methods (it's overloaded) is not
virtual or abstract. This method would most likely need to be modified by
any class inheriting DbCommand. On the other hand, DbCommand.ExecuteScaler
is abstract, which is exactly what I would thought DbCommand.ExecuteReader
methods should be.

What would the default implemenation of DbCommand.ExecuteReader() actually
do? Just pass back a DbDataReader object?

If I use the "new" keyword to replace the ExecuteReader methods, I'm scared
polymorphsim will be broken (and some of the factory stuff and other generic
coding won't work).
 
Jed Ozone said:
I'm converting over a ADO.NET provider I wrote in 1.1 .NET to 2.0. I
don't
understand why the DbCommand.ExecuteReader methods (it's overloaded) is
not
virtual or abstract. This method would most likely need to be modified by
any class inheriting DbCommand. On the other hand,
DbCommand.ExecuteScaler
is abstract, which is exactly what I would thought DbCommand.ExecuteReader
methods should be.

What would the default implemenation of DbCommand.ExecuteReader() actually
do? Just pass back a DbDataReader object?

If I use the "new" keyword to replace the ExecuteReader methods, I'm
scared
polymorphsim will be broken (and some of the factory stuff and other
generic
coding won't work).

The default DbCommand.ExecuteReader() functions call the protected
ExecuteDbDataReader() function, which is what you'll be implementing in your
derived class.

If you want to be complete however, you should use the "new" keyword and
override the two ExecuteReader() functions as well so they return your
type-specific data reader. For example, here's my implementation of those 3
functions:

protected override DbDataReader ExecuteDbDataReader(CommandBehavior
behavior)
{
return ExecuteReader(behavior);
}

public new SQLiteDataReader ExecuteReader()
{
return ExecuteReader(CommandBehavior.Default);
}


public new SQLiteDataReader ExecuteReader(CommandBehavior behavior)
{
InitializeForReader();

SQLiteDataReader rd = new SQLiteDataReader(this, behavior);
_activeReader = rd;

return rd;
}
 
Ah, that makes perfect sense. I couldn't find any documentation on this on
MDSN, but your explanation was perfect. Much thanks!
 
Robert Simpson said:
The default DbCommand.ExecuteReader() functions call the protected
ExecuteDbDataReader() function, which is what you'll be implementing in
your derived class.

If you want to be complete however, you should use the "new" keyword and
override the two ExecuteReader() functions as well so they return your
type-specific data reader.

Why would you want to do that? Then you'll run different code depending on
the type the client binds to. You definitely wouldn't want different
behavior in these cases, so why hide the method?
For example, here's my implementation of those 3 functions:

protected override DbDataReader ExecuteDbDataReader(CommandBehavior
behavior)
{
return ExecuteReader(behavior);
}
public new SQLiteDataReader ExecuteReader()
{
return ExecuteReader(CommandBehavior.Default);
}


public new SQLiteDataReader ExecuteReader(CommandBehavior behavior)
{
InitializeForReader();

SQLiteDataReader rd = new SQLiteDataReader(this, behavior);
_activeReader = rd;

return rd;
}

??????

David
 
David Browne said:
Why would you want to do that? Then you'll run different code depending
on the type the client binds to. You definitely wouldn't want different
behavior in these cases, so why hide the method?

No you won't. Please explain how the behaviors are different ...

Scenario 1: User calls DbCommand.ExecuteReader(...)
Result: The base DbCommand function calls the overridden
ExecuteDbDataReader() which in turn calls my "new" ExecuteReader() and
returns a type-specific SQLiteDataReader.

Scenario 2: User calls SQLiteCommand.ExecuteReader(...)
Result: The "new" ExecuteReader function returns a type-specific
SQLiteDataReader.

Both scenarios drill down to my "new" implementation of ExecuteReader()
which returns a type-specific DataReader for my object. Scenario 1 has 1
extra function call, and the base DbCommand function casts the resulting
object down to a DbDataReader, but that's what its supposed to do anyway.

Perhaps you should break out Lutz's Reflector and have a gander at
SqlClient.SqlCommand and see how they do it ... you'll find my method
remarkably similar to Microsoft's approach.

Robert
 
Robert Simpson said:
No you won't. Please explain how the behaviors are different ...

The two aren't different, if you code them to do the exact same thing. But
why would you want to hide a method with an implementation that does the
exact same thing as the overriden method?

How do you know that DbCommand.ExecuteReader doesn't do anything other than
call the protected method? What if this changes in the future?
Scenario 1: User calls DbCommand.ExecuteReader(...)
Result: The base DbCommand function calls the overridden
ExecuteDbDataReader() which in turn calls my "new" ExecuteReader() and
returns a type-specific SQLiteDataReader.

Scenario 2: User calls SQLiteCommand.ExecuteReader(...)
Result: The "new" ExecuteReader function returns a type-specific
SQLiteDataReader.

Both scenarios drill down to my "new" implementation of ExecuteReader()
which returns a type-specific DataReader for my object. Scenario 1 has 1
extra function call, and the base DbCommand function casts the resulting
object down to a DbDataReader, but that's what its supposed to do anyway.

So you have two different code paths to maintain that both do exactly the
same thing.

David
 
David Browne said:
The two aren't different, if you code them to do the exact same thing. But
why would you want to hide a method with an implementation that does the
exact same thing as the overriden method?

The point is, they do not do the same thing. One returns a DbDataReader, and the other returns a SQLiteDataReader. While you can implicitly cast a SQLiteDataReader to a DbDataReader, you cannot implicitly cast in the other direction and hence the different codepaths are distinct in purpose.
How do you know that DbCommand.ExecuteReader doesn't do anything other than
call the protected method? What if this changes in the future?

It cannot and will not change. DbCommand.ExecuteReader() has no knowledge of implementation, and leaves implementation up to the provider via the ExecuteDbDataReader(). Its behavior is documented and will not change. If in the future Microsoft determines that additional functionality is needed, they will create a new function for it and will not break the millions of deployed apps already out in the wild.
So you have two different code paths to maintain that both do exactly the
same thing.

There is one code path, and they do not do the same thing. All paths converge at the new type-specific ExecuteReader(CommandBehavior) function. There is no duplication of anything.

Lets start by examining two sample usages, one with the built-in Microsoft Sql Server provider, and the other with mine:

// SqlClient version
using (SQCommand cmd = sqlcnn.CreateCommand())
{
cmd.CommandText = "SELECT GetDate()";
using(SqlDataReader reader = cmd.ExecuteReader())
{
// etc etc
}
}

// SQLite version
using (SQLiteCommand cmd = sqlitecnn.CreateCommand())
{
cmd.CommandText = "SELECT * FROM sqlite_master";
using(SQLiteDataReader reader = cmd.ExecuteReader())
{
// etc etc
}
}

The above code samples are nearly identical, and are common usage scenarios that happen frequently. If I were to remove the "new" ExecuteReader functions in my implementation of SQLiteCommand, then the above code would no longer compile against my provider. The reason it would no longer compile is because there would be no version of ExecuteReader() that returns a SQLiteDataReader and the call to cmd.ExecuteReader() would require an explicit up-cast.

Leaving out the "new" ExecuteReader() functions would be inconsistent with the way the other ADO.NET providers behave. The rule seems to be, "be specific when being specific, and be generic when being generic."

In either case, the code below will compile in any circumstance, but using the "be generic when being generic" rule:

using (DbCommand cmd = sqlitecnn.CreateCommand())
{
cmd.CommandText = "SELECT * FROM sqlite_master";
using (DbDataReader reader = cmd.ExecuteReader())
{
// etc etc
}
}

If you'd like more examples, look at the following:

SqlConnection.CreateCommand() vs. DbConnection.CreateCommand()
SqlCommand.CreateParameter() vs. DbCommand.CreateParameter()

In each of these cases as well, the SqlConnection and SqlCommand objects have overridden using "new" the CreateXXX() functions in addition to overriding the virtual base implementations (CreateDbParameter and CreateDbCommand) so as to return a generic or type-specific object without forcing the user to explicitly upcast.

It's merely a matter of convenience to the user so they can be as specific or as generic as they like without forcing them to explicitly cast the return values.

Robert
 
Bah ... typo in that SqlClient example ...

// SqlClient version
using (SqlCommand cmd = sqlcnn.CreateCommand())
{
cmd.CommandText = "SELECT GetDate()";
using(SqlDataReader reader = cmd.ExecuteReader())
{
// etc etc
}
}
 
Back
Top