-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Nullability annotations for main System.Data.Common types #689
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Updated to follow latest proposal at dotnet/runtime#689.
Updated to follow latest proposal at dotnet/runtime#689.
This will be needed for annotating the entire System.Data.Common, by dependency we are not that close to annotate System.Data.Common yet, but adding reference to main issue https://github.com/dotnet/corefx/issues/40623 for tracking |
@@ -2291,19 +2316,24 @@ public abstract partial class DbParameterCollection : System.MarshalByRefObject, | |||
public abstract object SyncRoot { get; } | |||
object System.Collections.IList.this[int index] { get { throw null; } set { } } | |||
object System.Data.IDataParameterCollection.this[string parameterName] { get { throw null; } set { } } | |||
int System.Collections.IList.Add(object? value) { throw null; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roji Your PR is adding new public APIs in the ref file. Please make sure to document them with triple slash comments so the reviewers can sign off your change. We want to make sure all PRs introducing new public APIs get properly documented before getting merged.
These are explicit interface implementations (EIIs) so it's ok if you copy-paste the documentation for the original APIs. You can add a remark with a message that looks like this (replace the two [keywords]):
<remark>
This member is an explicit interface member implementation. It can be used only when the [instanceName] instance is cast to an [interfaceName] interface.
</remark>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @carlossanlop, this PR is on hold for now as I complete other work, but when I get back to it I'll definitely make sure to do the proper documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@carlossanlop I've revived this and added XML docs following the model for DataSet.IListSource.GetList which you linked to. If you prefer me to copy the actual docs from IList I can do that (or do we may support <inheritdoc />
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@carlossanlop, rather than requiring such copy/paste, can we just automatically handle that for explicitly implemented members?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI have removed the new-api-needs-documentation (as the boilerplate XML docs have been copied in).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DocsPortingTool is now able to detect an EII API and automatically add the sentence I suggested if it does not yet exist in triple slash.
Feel free to remove that sentence from this PR. I wrote the original suggestion back when the feature wasn't available in the tool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to hear that was added. Thanks.
Can we close it then and open it again when it's ready to be reviewed/merged? Thanks. |
@stephentoub closing, am assuming this is to do things bottoms-up as in #2339? |
That's how we prefer to do it, yes. But we also don't want "pull" requests sitting stale that aren't ready to be "pulled", and you stated it was on hold indefinitely :-) |
No problem, am indeed busy with other stuff at the moment anyway, and it makes sense for this to wait until the stack is annotated underneath. For anyone else, please feel free to take a look at this and also to comment accordingly, we'll pick this up soon from where we left off. |
Hey @roji, we've annotated most of the dependencies this has, so if you're interested in pushing this forward and want to open a PR with the annotations, it's a reasonable time. Thanks! |
I've rebased this and made some adjustments - it should be ready for reviewing. |
e1c85f4
to
200bca9
Compare
src/libraries/System.Data.Common/src/System/Data/Common/DbProviderFactory.cs
Outdated
Show resolved
Hide resolved
200bca9
to
9e9db8d
Compare
78891f1
to
9eb6dc1
Compare
PS Build is passing aside from some Helix-only failures (WASM mono assertion issue...) |
We should not do that. Either we're confident that no concrete implementations will return null (either explicitly or because they don't override the base method and get the base's null-returning behavior), or we're not. If we're confident it never happens, then the methods can be non-nullable and can be changed to throw. If we're not confident, then annotating them as non-nullable but returning null is making things worse: we're claiming to consumers they can trust they'll never get back null, but that's simply not true. Just to make sure it's clear, too, concrete providers can be annotated to return non-nullable even when the base virtual is null-returning. So, for example, even if we have: public abstract class DbProviderFactory
{
public virtual DbCommand? CreateCommand() => null;
...
} we can still have: public sealed class SqlClientFactory : DbProviderFactory, IServiceProvider
{
public override DbCommand CreateCommand() => new SqlCommand();
} Anyone using SqlClientFactory directly would see that CreateCommand returns a non-nullable DbCommand; only when using the base provider would they see that it returns nullable. We also don't need to do the same thing for every method in question. The ones we're talking about are these 8? public abstract class DbProviderFactory
{
public virtual DbCommand CreateCommand() => null;
public virtual DbCommandBuilder CreateCommandBuilder() => null;
public virtual DbConnection CreateConnection() => null;
public virtual DbConnectionStringBuilder CreateConnectionStringBuilder() => null;
public virtual DbDataAdapter CreateDataAdapter() => null;
public virtual DbParameter CreateParameter() => null;
public virtual CodeAccessPermission CreatePermission(PermissionState state) => null;
public virtual DbDataSourceEnumerator CreateDataSourceEnumerator() => null;
} plus public abstract class DbConnection
{
protected virtual DbProviderFactory DbProviderFactory => null;
} right? Looking at https://source.dot.net and https://referencesource.microsoft.com/, I see every single code path that ends up hitting this (generally via DbProviderFactories.GetProvider) then null checks the result and throws if null was returned. And looking around on github, I see lots of code that just assumes it's not null and dereferences the result without checking. So (and please correct me if I'm wrong), that other than the type itself using its own protected override, null being returned is already effectively considered an error. If that's true, DbConnection.DbProviderFactory could be made non-nullable and changed to throw with minimal impact. Looking just at dotnet/runtime, neither Code Access Security is deprecated, so You mentioned that "CreateCommandBuilder and CreateDataAdapter return null on Microsoft.Data.Sqlite" (as can CreatePermission and CreateDataSourceEnumerator, per https://github.com/dotnet/efcore/blob/master/src/Microsoft.Data.Sqlite.Core/SqliteFactory.cs). If they return null for such a prominent implementation, that's a warning sign. On to of that, if memory serves these were introduced later than the other methods, which means implementations based on the original DbProviderFactory surface area wouldn't have overridden these because they didn't exist; that's exemplified in examples like https://github.com/mysql-net/MySqlConnector/blob/master/src/MySqlConnector/MySqlConnectorFactory.cs and https://github.com/jonwagner/Insight.Database/blob/master/Insight.Database.Core/DbConnectionWrapperProviderFactory.cs, where these methods won't be overridden when targeting surface area like .NET Standard 1.6 or .NET Core 1.1. So... CreateCommandBuilder and CreateDataAdapter should return nullable. Here's an implementation that doesn't override either CreateParameter or CreateConnectionStringBuilder: I couldn't find any implementations that returned null from CreateConnection/CreateCommand. Maybe we could get away with making those two non-nullable and throwing from the base. If all of the above makes sense, that would leave us with: public abstract class DbProviderFactory
{
public virtual DbCommand CreateCommand();
public virtual DbConnection CreateConnection();
public virtual DbCommandBuilder? CreateCommandBuilder();
public virtual DbConnectionStringBuilder? CreateConnectionStringBuilder();
public virtual DbDataAdapter? CreateDataAdapter();
public virtual DbParameter? CreateParameter();
public virtual CodeAccessPermission? CreatePermission(PermissionState state);
public virtual DbDataSourceEnumerator? CreateDataSourceEnumerator();
}
public abstract class DbConnection
{
protected virtual DbProviderFactory DbProviderFactory { get; }
} but I'd like more data from @marklio's data sources, if possible. And with most of the above returning nullable, it may just make sense to do the simple / honest / non-breaking thing, and return nullable from all of them, reflecting exactly the implementation we have today. |
Sure, but the whole point of DbProviderFactory is for people to use it without referencing SqlClient types directly (e.g. SqlProviderFactory). If I'm coding directly against my ADO.NET provider (as most people do) - rather than writing a database-portable application/library - I can just instantiate an SqlConnection directly rather than using the factory. So the value of the annotations here is mostly about what happens in the abstract base class (DbProviderFactory).
Yes.
I absolutely agree. Every ADO.NET provider must have an implementation of DbProviderFactory, and its DbConnection.DbProviderFactory must return that. It's good to know that usage confirms this, I will change it to throw. Regarding the rest... I agree that for CreatePermission it doesn't really matter. On the others, I'd argue that in most cases, the providers not implementing CreateParameter and the like are buggy, i.e. not respecting the ADO.NET contract. In other words, it doesn't seem possible to successfully use these providers via the DbProviderFactory abstraction; this might not be a problem for users since the providers in question are probably not being used by portable applications. If that's true, then changes to DbProviderFactory won't impact these users in any case. To summarize, changing to throw will only affect database-portable consumers which call CreateParameter (and the like), and check the result for null. But if null is indeed returned, it's not clear how that application can function properly. However, I can also see what you're seeing. If the only remaining question here is about CreateConnection/CreateCommand, then as you suggest let's just annotate all of them as nullable and be done with it. Let me know what you decide. PS Unrelated: is it OK to merge this PR with the unrelated failing WASM tests on Helix? I've just relaunched it in case it's flakiness. |
That's the problem: they are. There's nothing about this in the docs that I can see, and the base implementations (which by definition adhere to the contract) are returning null. Is there some place where the contract is more explicitly defined? Are there docs anywhere that state null shouldn't be returned? |
No, ADO.NET is notoriously badly-defined - if our bar for changing anything is a clear sentence in specs/docs, then we definitely don't have that. I'll amend the PR to only throw for DbConnection.DbProviderFactory. |
b80ae8f
to
7acc3f4
Compare
Sounds like we've landed on the following principles:
How compatible are .NET Framework implementations with the System.Data.Common types in Core? My conclusions below are based on an assumption that such implementations fall into our "best effort" compatibility, but the ADO.net API shapes in Core match well enough to expect them to generally work. If that's true, there are 2405 (2314 if you only count public types) NuGet package ids (each with many versions) that contain assemblies that define at least 1 type that extends DbConnection (query below for those with access). These definitely include the "official" providers, some of which have both a .NET Core and .NET Framework version (ex. ODP). Many of the others appear to be wrappers, but some are constructions that allow different things (ex. datastructures, files, etc.) to behave like databases either for testing, or seemingly as parts of components. It seems entirely possible that such things "conform poorly" to our expectations of an ADO.net provider. If our goal is to remain compatible and consistent with all the ADO.net-ish components and their consumers that are out there using this extensibility model, my opinion would be to not introduce any behavioral changes, and simply annotate the existing behaviors. At the point where we've given up on the broader set of changes, I'm not sure I see value in the one or two places where we might save a null check (but also might introduce a null-ref exception). I'm happy to pull some more data here if anyone thinks they need to see more. I'm also happy to help folks who would like to dig in deeper themselves.
|
The principles are already enumerated in https://github.com/dotnet/runtime/blob/master/docs/coding-guidelines/api-guidelines/nullability.md. |
Sorry, I wasn't suggesting I had come up with new principles, or that others didn't exist. I was stating the things I felt were key to guiding resolution of the specific outstanding questions here. |
7acc3f4
to
c09b807
Compare
Thanks for the input @marklio, that's indeed the direction we're going. @stephentoub I've changed the PR as discussed - |
@roji can you please also create a docs breaking change issue using this template https://github.com/dotnet/docs/issues/new?template=dotnet-breaking-change.md |
FYI after discussion with @ajcvickers I'll simply revert the DbConnection.DbProviderFactory change as well. The value is very low on its own, and it just doesn't seem to justify communicating on it as a breaking change etc. |
c09b807
to
8202aab
Compare
8202aab
to
cb9ee65
Compare
Thanks, @roji. I know this didn't land exactly where you wanted. Thank you for working on it. |
No worries at all, it's also a matter of me understanding the exact bar for breaking changes, our nullability guidelines etc. Am now wrapping up a PR for the rest of System.Data, and plan to do System.Data.Odbc, System.Data.OleDb, System.ComponentModel.Annotations after that. |
General Notes
This PR is a proposal for annotating the main types in System.Data.Common (S.D.C). It focuses on types necessary to create ADO.NET providers, and leaves out various non-essential types; this will be revisited at a later time.
Unlike the usual NRT annotation exercise, here we're annotating a provider abstraction; there is very little actual logic in S.D.C we can consult to determine what should be nullable. In addition, specifications around nulls are unfortunately rarely present in the documentation. As such, the behavior of existing major providers was used as a guidance for annotation, as a "de-facto standard". Four providers were checked: Microsoft.Data.SqlClient, Npgsql, MySQL Connector and Microsoft.Data.SQLite. The AdoNetApiTest suite was used extensively to map out behavior across providers; this exercise also resulted in many test additions to that suite as well.
The goal here is to gather feedback from ADO.NET driver developers and other people working in the .NET database space, as well as from BCL experts and people experienced in NRT annotation.
/cc @cheenamalhotra @David-Engel @saurabh500 @Wraith2 @bgrainger @ErikEJ @FransBouma @mgravell @bricelam @ajcvickers @AndriySvyryd
/cc @stephentoub @cartermp @rynowak
Null-returning base method implementations
There are unfortunately several cases where a base S.D.C implementation returns null for a type that providers must provide. Examples include methods on DbProviderFactory (e.g. CreateConnection), DbConnection.DbProviderFactory, etc.; these properties really should have been made abstract, or at least throw instead of returning null.
We have three options:
Depending on our backwards-compatibility bar, I believe we should at least do 2, possibly 1.I believe the breaking change is worth it (option 1).
See some previous discussion in https://github.com/dotnet/corefx/issues/35535, and specific discussion below.
De-facto normalized string properties
There are several cases of string properties which are documented to be an empty string by default, and which providers generally normalize to empty string when null is set:
As per the BCL nullability guidelines:
As the de-facto provider behavior is to allow nulls (and normalize to empty string), these properties are annotated as non-nullable with
[AllowNull]
.Note that the same holds for the setter of
DbConnectionStringBuilder.this[]
: setting a keyword to null means removing it.Specific Type-By-Type Notes
This section details some annotation details which are noteworthy. Where annotation was straightforward no notes were made.
DbDataReader/IDataReader/DataReaderExtensions
DbTransaction/IDbTransaction
DbTransaction.Connection: always non-null before commit/rollback, always null afterwards; so annotated it as nullable. It would have been better for DbTransaction to transition to a disposed state immediately after commit/rollback, in which case the Connection would have been non-nullable.
DbProviderFactories
No specific comments
DbProviderFactory
DbCommand
[AllowNull]
as per guidelines (see note above).DbConnection
[AllowNull]
as per guidelines (see note above).DbConnectionStringBuilder
[AllowNull]
as per guidelines (see note above).[AllowNull]
as per guidelines (see note above).DbParameter
[AllowNull]
as per guidelines (see note above).DbParameterCollection / IDataParameterCollection
DbColumn
SELECT 1
there is no table, schema...).Edit history