-
Notifications
You must be signed in to change notification settings - Fork 291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow passing null for SqlParameter value #991
Comments
You, the developer, do. By extension your users do. Language null and sql null aren't conceptually the same thing and they don't directly map onto one another so you are required to indicate what should happen if a parameter is null:
This situation would be different if generics and nullable type annotations had existed at the time that the api was designed, there would probably be no need for DBNull at all and mapping would be easier. |
I don't buy it. At minimum the error could at least say something like...
Of course, this goes back to the problem of, if the library knew what I was doing why is it making me work for it? exec sp_executesql N'select @',N'@ int'
So it appears that the driver sends the metadata, but then doesn't bother sending the actual value. The error sucks from sql server, but here I can at least see that oh hey, I never did specify a value for I almost always end up using a tool like Dapper, or efcore which will magically wrap away this behavior. e.g.
In my example, and my real code that I needed to do this I was using a nullable int.
Null is an entirely valid for sql queries and for sql databases, so the questioning is disingenuous. E.g. you very well might want to leave insert into dbo.address(user_id,address_line,address_line2,city,state,zip)
values(@userId,@addressLine,@addressLine2,@city,@state,@zip); And similarly, you may want to query data for keys that contains null. Like finding all running instances of a particular job type for a date that have not completed yet. select batch_id
from dbo.batch_run
where exists (
select job_type,as_of_date,completion_time
intersect
select @jobType,@asOfDate,@completionTime
) I am unsure what a more recently written driver like npgsql does, but I'm going to guess that sending null with a dbtype of int isn't an error with an obtuse message. |
It doesn't. You didn't tell it. SqlParameter.Value is of object type and you can put anything you want in there. You want to pass in nothing and have it infer that language null means database null but that may not be the case because it could equally be an error of input data. You, the programmer, are supposed to give it unambiguous instructions which is what DBNull is for. DBNull is a value which indicates to the library that yes you really do mean to send null to the database.
I'm pretty sure that because it uses generically typed parameter classes it is able to make safer assumptions and if you provide a language null to a field which is declared as being able to hold a null it will send it as database null. SqlClient uses object for the value container because it predates generics and so DBNull is needed as a sentinel value. /cc @roji |
Npgsql does have a generic parameter API (e.g. to avoid boxing), but this isn't standard ADO.NET (see dotnet/runtime#17446), and Npgsql also supports the standard non-generic API. One tricky point with the generic API, is that it's not possible to use it to write nulls for value types. I've never quite investigated the entire question of ADO.NET, .NET null and database null, or how feasible it would be to allow .NET null to be interpreted as database null. IIRC the difference is significant in some places, e.g. when inserting a new row, .NET null means "use the column's default value" whereas database null means database null (but I'm not 100% sure on this anymore). I do know that it's not something we can just consider doing without a lot of careful analysis. IMO this is also something that needs to be considered from a cross-database perspective, and not as a pure SqlClient change. |
I guess the question then if I use a standard DbParameter in npgsql, does it consider null to be It seems most client abstraction libraries like EFCore and Dapper will hide the difference for the user when using parameterized queries. Now I suppose in the case of a store procedure call, a parameter that has no value will invoke the default. (analogous to doing the following in sql. exec sp_executesql N'select @',N'@ int=null'
-- is equivalent to the following
exec sp_executesql N'select @',N'@ int=null',@=default Of course, if this is the desired nonsense behavior than perhaps it could be solved with a boolean on The crux I keep hearing is that sqlclient was written in .net 1.0 times, and all of it's cruft and unwieldiness is an unfortunate consequence of its legacy. |
Hi @mburbea Yes I agree driver is generating: I tried with |
@mburbea an important point here, is that if you change an ADO.NET driver to suddenly treat language null as database null (instead of default), you're potentially breaking a whole lot of programs out there which rely on this behavior (which has been around since forever). Of course, if there's a reliable way to detect the error situation and error with a better message, that's always a good idea (but I'm not sure that's the case here). |
Just out of interest, with generic parameter types if you add |
I'm going to say that the behavior is safe to treat null as I am also not suggesting to make this change in the Since procedures, may want this behavior where unsupplied parameters are treated as default, and I have no idea what the I suppose if anyone was testing for this exception, they would break. Like I acknowledged earlier, it's a breaking change, but outside of testing for an exception, I can't imagine anyone relying on this behavior. |
The problem with being part of the runtime for so long is that I can be certain that someone somewhere has relied on the current behaviour and a change will break it and absolutely ruin some poorly paid maintenance developers day/week/month. Much like the BCL behavioural changes are extremely hard to justify in old apis, subtle behavioural changes in this library even though it's optional are hard to justify. The upgrade path from System to Microsoft versions needs to be smooth to allow adoption in LOB software. Now, when we get generic parameters like postgres has that'll be all new surface area with no need to use DBNull and then we can do things more appropriate for the language and runtime we have today. |
The runtime has already made some breaking changes to allow behavior that has always thrown an exception. var dict = new Dictionary<int, int> { [0] = 0, [1] = 1 };
foreach (var kvp in dict)
{
if (kvp.Key == 0) dict.Remove(kvp.Key);
} In .net framework 4.8 this throws an exception ( Collection was modified; enumeration operation may not execute). In .net core 3/.net 5 it does not. Again, since this is only an exception, and it is very unlikely to be caught in anything but a unit test testing for an exception they still made the change. I think that being so cautious means that the library can never evolve and we are simply stuck with crappy behavior that nobody uses, and basically everyone is required to adopt an abstraction layer. |
FYI (I guess this is known workaround already) cmd.Parameters.Add(new SqlParameter("@", DBNull.Value) { DbType = DbType.Int32 });
Server receives: |
I know, I made that change 😁 dotnet/coreclr#18854 I'm also sure that it will have broken some code being ported from netfx. going from netcore to netfx is a porting process and that particular break was documented and called out in release notes iirc. Going from System to Microsoft versions of this library really needs not to contain breaking changes if at all possible. I'm not saying it won't (I know cancellation exceptions are going to change soon) but the bar for approval of those changes is pretty high. In context I don't think there is a lot that can be done with the legacy parameter mechanism without high risk of breaking someone's code, even if they wrote bad code I still don't want to break it if I can help it. |
@Wraith2, ha didn't realize you made that change. That's pretty funny as that was the first breaking change that came to my mind. (and a welcome one). Edit: I guess like #255, we worst case can introduce a switch and default it to false in .net framework and true in .netcore side? Also nit: |
#255 I can probably fix quite easily because of the suggested appcontext switch it just wasn't on my radar. |
I don't see how CommandType is related, or how it's safe. Unless I'm missing something, if you create a SqlCommand today with CommandType.Text, set text to IsDbNull{Async} is for the reading side, which is a different discussion - up to now we've been discussing the writing side.
Changing behavior to not throw is by definition not considered a breaking change. What you're proposing would change the meaning of existing, working code to do something entirely different. To be honest, I don't really see the point of this discussion... The current way things work don't block anyone - as @cheenamalhotra mentioned above, users simply need to pass |
@roji, the CommandType matters because default does not work as you described in this case. create table dbo.t(c nvarchar(4000) default 'horse'); If I try this... using (var cmd = new SqlCommand("insert into dbo.t(c)values(@)", conn))
{
cmd.Parameters.AddWithValue("@", null);
await cmd.ExecuteNonQueryAsync();
} I get that cryptic exception
However, if I use a store procedure which does have a default value supplied for a parameter like:: create or alter procedure dbo.p(@ nvarchar(4000)='horse')
as
insert into dbo.t(c) values(@); Then the following works as you describe (note that I have to set the mode to procedure). using (var cmd = new SqlCommand("dbo.p", conn) { CommandType = CommandType.StoredProcedure })
{
cmd.Parameters.AddWithValue("@", null);
await cmd.ExecuteNonQueryAsync();
} This is why I mentioned to keep the behavior of null meaning default for procedures. For text where it can ONLY be an error, I think we can unblock the change safely. I say it's an error and an odd design because lots of people run into this problem. (Search for the exception on stackoverflow if you think i'm alone (~13,000 results)). I disagree with the viewpoint that it doesn't block people so it shouldn't be fixed. Ultimately, many libraries like efcore, dapper, and now my internal layer now has code akin to |
I can't think of any case where calling AddWithValue with a null value is ever the right thing to have done and I think I'd have said it should throw an exception. We can't change that either. If the exception message needs to be changed to better inform people about the problem then that can probably be done but changing the behaviour will cause a massive compatibility problem. |
@Wraith2, I'm still trying to find this case where the behavior works as you describe such that it is a breaking change. How does this impact compatability? If we make the change only occur when |
Also to add, when trying to mock this in SSMS, it seems like setting exec sp_executesql N'insert into t1 values (@1, @2)',N'@1 nvarchar(4000),@2 int',@1=default,@2=NULL
-- throws: The parameterized query '(@ nvarchar(4000))select @' expects the parameter '@', which was not supplied. It's also different than passing value directly in the query in a non-parameterized way or via another procedure internally (like explained by @mburbea above), and that's out of context for The stored procedure calls exec sp_executesql N'dbo.p',N'@ nvarchar(4000)',@=default
-- The parameterized query '(@ nvarchar(4000))dbo.p' expects the parameter '@', which was not supplied. |
I feel like I'm repeating myself, but I still am not seeing the big problem here. Yes - the API might not be ideal, and new users to ADO.NET are likely to bump into this. However, the error message seems very explicit to me: To summarize, I really am not against improving things in general, and even think that the occasional breaking change makes sense if it serves a very valuable purpose. IHMO that simply isn't the case here - the value of this change seems very small weighed against the larger potential impact. |
@roji, to answer a few of your points:: var dt = new DataTable { Columns = { { "C", typeof(string) } }, Rows = { { new object[] { null } } } };
using var bulkCopy = new SqlBulkCopy(conn, SqlBulkCopyOptions.KeepNulls, null){DestinationTableName = "dbo.t"};
await bulkCopy.WriteToServerAsync(dt);
using (var cmd = new SqlCommand(@"insert into t select * from @",conn))
{
cmd.Parameters.Add(new("@", dt) { SqlDbType = SqlDbType.Structured, TypeName = "dbo.tvp" });
await cmd.ExecuteNonQueryAsync();
} I think the message is unclear. I supplied the parameter |
I do agree that adding some text to help guide users in the direction of DBNull.Value makes sense, along the lines of the message you propose. But I'd be careful about doing a behavioral change over this point without some thorough research into the other providers and general null handling in ADO.NET. |
FYI this bug still exists as of Jan 2023. For anyone who ended up on this GitHub issue from a web search like I did, I solved it by writing a little wrapper:
Then you can call it like so:
|
@ScottRFrost this is not a bug; this is how the ADO.NET API was designed to work. Of course, that doesn't mean it can't be changed at some point. |
@Wraith2 wrote:
Why are we making this distinction between "language null" and "database null", but not between:
Developers can pass in all these other values as-is. If a language didn't have a null token, then something would be required to indicate that the developer intends to pass a null instead of, say, an empty string. But when a language has a null token, as is the case in C#, "language null" and "database null" are the same thing and they mean "no value" for the variable or column. The error message saying a parameter wasn't provided sounds like it wasn't provided at all, not with any value including the non-value null, which in "language" terms should mean AddWithValue wasn't even called for that parameter. I understand the point about not changing the API and breaking things for people, so I have these suggestions to contribute:
|
It is legal and common to call AddWithValue to create the parameter and then later, often in a loop, set the value to a non-null. value. This is because it's often awkward to call a different parameter ctor or Add overload, AddWithValue is quick common and easy. Changing to an exception now would break code working code at runtime, not a fun experience for users.
This could work. It's something that should probably be added to the DbCommand with a default implementation that can be overloaded by each provider rather than something we do specifically in SqlClient. What do you think @roji ? |
Is your feature request related to a problem? Please describe.
Currently, if you pass
null
to the Value in aSqlParameter
constructor or use theAddWithValue
method, you receive a rather cryptic exception message::e.g.
The message is entirely unclear, and makes you have to google it, only to discover that the correct resolution of this problem is to replace
null
withDbNull.Value
.There are many, many threads about this on stackoverflow and various forums.
Describe the solution you'd like
When you create a parameter with
null
, it internally stores downDbNull.Value
.This is technically a breaking change, but I'm not sure who actually benefits from this obtuse behavior where null is an exception rather than doing the logical thing.
Describe alternatives you've considered
Additional context
The text was updated successfully, but these errors were encountered: