diff --git a/src/libraries/System.Data.Common/ref/System.Data.Common.cs b/src/libraries/System.Data.Common/ref/System.Data.Common.cs index fc4963afd6a9e..7f8e500b823aa 100644 --- a/src/libraries/System.Data.Common/ref/System.Data.Common.cs +++ b/src/libraries/System.Data.Common/ref/System.Data.Common.cs @@ -2405,6 +2405,24 @@ protected DbDataRecord() { } System.ComponentModel.PropertyDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetProperties(System.Attribute[]? attributes) { throw null; } object System.ComponentModel.ICustomTypeDescriptor.GetPropertyOwner(System.ComponentModel.PropertyDescriptor? pd) { throw null; } } + public abstract class DbDataSource : IDisposable, IAsyncDisposable + { + public abstract string ConnectionString { get; } + protected abstract System.Data.Common.DbConnection CreateDbConnection(); + protected virtual System.Data.Common.DbConnection OpenDbConnection() { throw null; } + protected virtual System.Threading.Tasks.ValueTask OpenDbConnectionAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; } + protected virtual System.Data.Common.DbCommand CreateDbCommand(string? commandText = null) { throw null; } + protected virtual System.Data.Common.DbBatch CreateDbBatch() { throw null; } + public System.Data.Common.DbConnection CreateConnection() { throw null; } + public System.Data.Common.DbConnection OpenConnection() { throw null; } + public System.Threading.Tasks.ValueTask OpenConnectionAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Data.Common.DbCommand CreateCommand(string? commandText = null) { throw null; } + public System.Data.Common.DbBatch CreateBatch() { throw null; } + public void Dispose() { throw null; } + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + protected virtual void Dispose(bool disposing) { throw null; } + protected virtual System.Threading.Tasks.ValueTask DisposeAsyncCore() { throw null; } + } public abstract partial class DbDataSourceEnumerator { protected DbDataSourceEnumerator() { } @@ -2606,6 +2624,7 @@ protected DbProviderFactory() { } public virtual System.Data.Common.DbDataAdapter? CreateDataAdapter() { throw null; } public virtual System.Data.Common.DbDataSourceEnumerator? CreateDataSourceEnumerator() { throw null; } public virtual System.Data.Common.DbParameter? CreateParameter() { throw null; } + public virtual System.Data.Common.DbDataSource CreateDataSource(string connectionString) { throw null; } } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed partial class DbProviderSpecificTypePropertyAttribute : System.Attribute diff --git a/src/libraries/System.Data.Common/src/Resources/Strings.resx b/src/libraries/System.Data.Common/src/Resources/Strings.resx index c1a4dee0c36b0..5199442caa450 100644 --- a/src/libraries/System.Data.Common/src/Resources/Strings.resx +++ b/src/libraries/System.Data.Common/src/Resources/Strings.resx @@ -156,6 +156,8 @@ Provided range for time one exceeds total of 14 hours. Kind property of provided DateTime argument, does not match 'hours' and 'minutes' arguments. A DataColumn of type '{0}' does not support expression. + Connection and transaction access is not supported on batches created from DbDataSource. + Connection and transaction access is not supported on commands created from DbDataSource. Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints. Collection itself is not modifiable. The given name '{0}' matches at least two names in the collection object with different cases, but does not match either of them with the same case. diff --git a/src/libraries/System.Data.Common/src/System.Data.Common.csproj b/src/libraries/System.Data.Common/src/System.Data.Common.csproj index c4ac48cf19ed3..8c1413c2612d4 100644 --- a/src/libraries/System.Data.Common/src/System.Data.Common.csproj +++ b/src/libraries/System.Data.Common/src/System.Data.Common.csproj @@ -210,6 +210,7 @@ + @@ -224,6 +225,7 @@ + diff --git a/src/libraries/System.Data.Common/src/System/Data/Common/DbDataSource.cs b/src/libraries/System.Data.Common/src/System/Data/Common/DbDataSource.cs new file mode 100644 index 0000000000000..c51c7f5950713 --- /dev/null +++ b/src/libraries/System.Data.Common/src/System/Data/Common/DbDataSource.cs @@ -0,0 +1,584 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Data.Common +{ + public abstract class DbDataSource : IDisposable, IAsyncDisposable + { + public abstract string ConnectionString { get; } + + protected abstract DbConnection CreateDbConnection(); + + protected virtual DbConnection OpenDbConnection() + { + var connection = CreateDbConnection(); + + try + { + connection.Open(); + return connection; + } + catch + { + connection.Dispose(); + throw; + } + } + + protected virtual async ValueTask OpenDbConnectionAsync(CancellationToken cancellationToken = default) + { + var connection = CreateDbConnection(); + + try + { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + return connection; + } + catch + { + await connection.DisposeAsync().ConfigureAwait(false); + throw; + } + } + + protected virtual DbCommand CreateDbCommand(string? commandText = null) + { + var command = CreateDbConnection().CreateCommand(); + command.CommandText = commandText; + + return new DbCommandWrapper(command); + } + + protected virtual DbBatch CreateDbBatch() + => new DbBatchWrapper(CreateDbConnection().CreateBatch()); + + public DbConnection CreateConnection() + => CreateDbConnection(); + + public DbConnection OpenConnection() + => OpenDbConnection(); + + public ValueTask OpenConnectionAsync(CancellationToken cancellationToken = default) + => OpenDbConnectionAsync(cancellationToken); + + public DbCommand CreateCommand(string? commandText = null) + => CreateDbCommand(commandText); + + public DbBatch CreateBatch() + => CreateDbBatch(); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + protected virtual ValueTask DisposeAsyncCore() + => default; + + private sealed class DbCommandWrapper : DbCommand + { + private readonly DbCommand _wrappedCommand; + private readonly DbConnection _connection; + + internal DbCommandWrapper(DbCommand wrappedCommand) + { + Debug.Assert(wrappedCommand.Connection is not null); + + _wrappedCommand = wrappedCommand; + _connection = wrappedCommand.Connection; + } + + public override int ExecuteNonQuery() + { + _connection.Open(); + + try + { + return _wrappedCommand.ExecuteNonQuery(); + } + finally + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up. + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedCommand.ExecuteNonQueryAsync(cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override object? ExecuteScalar() + { + _connection.Open(); + + try + { + return _wrappedCommand.ExecuteScalar(); + } + finally + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override async Task ExecuteScalarAsync(CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedCommand.ExecuteScalarAsync(cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + _connection.Open(); + + try + { + return _wrappedCommand.ExecuteReader(behavior | CommandBehavior.CloseConnection); + } + catch + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + } + + throw; + } + } + + protected override async Task ExecuteDbDataReaderAsync( + CommandBehavior behavior, + CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedCommand.ExecuteReaderAsync( + behavior | CommandBehavior.CloseConnection, + cancellationToken) + .ConfigureAwait(false); + } + catch + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + } + + throw; + } + } + + protected override DbParameter CreateDbParameter() + => _wrappedCommand.CreateParameter(); + + public override void Cancel() + => _wrappedCommand.Cancel(); + + [AllowNull] + public override string CommandText + { + get => _wrappedCommand.CommandText; + set => _wrappedCommand.CommandText = value; + } + + public override int CommandTimeout + { + get => _wrappedCommand.CommandTimeout; + set => _wrappedCommand.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => _wrappedCommand.CommandType; + set => _wrappedCommand.CommandType = value; + } + + protected override DbParameterCollection DbParameterCollection + => _wrappedCommand.Parameters; + + public override bool DesignTimeVisible + { + get => _wrappedCommand.DesignTimeVisible; + set => _wrappedCommand.DesignTimeVisible = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => _wrappedCommand.UpdatedRowSource; + set => _wrappedCommand.UpdatedRowSource = value; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var connection = _wrappedCommand.Connection; + + _wrappedCommand.Dispose(); + connection!.Dispose(); + } + } + + public override async ValueTask DisposeAsync() + { + var connection = _wrappedCommand.Connection; + + await _wrappedCommand.DisposeAsync().ConfigureAwait(false); + await connection!.DisposeAsync().ConfigureAwait(false); + } + + // In most case, preparation doesn't make sense on a connectionless command since prepared statements are + // usually bound to specific physical connections. + // When prepared statements are global (not bound to a specific connection), providers would need to + // provide their own connection-less implementation anyway (i.e. interacting with the originating + // DbDataSource), so they'd have to override this in any case. + public override void Prepare() + => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + + public override Task PrepareAsync(CancellationToken cancellationToken = default) + => Task.FromException(ExceptionBuilder.NotSupportedOnDataSourceCommand()); + + // The below are incompatible with commands executed directly against DbDataSource, since no DbConnection + // is involved at the user API level and the DbCommandWrapper owns the DbConnection. + protected override DbConnection? DbConnection + { + get => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + set => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + } + + protected override DbTransaction? DbTransaction + { + get => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + set => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + } + } + + private sealed class DbBatchWrapper : DbBatch + { + private readonly DbBatch _wrappedBatch; + private readonly DbConnection _connection; + + internal DbBatchWrapper(DbBatch wrappedBatch) + { + Debug.Assert(wrappedBatch.Connection is not null); + + _wrappedBatch = wrappedBatch; + _connection = wrappedBatch.Connection; + } + + public override int ExecuteNonQuery() + { + _connection.Open(); + + try + { + return _wrappedBatch.ExecuteNonQuery(); + } + finally + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedBatch.ExecuteNonQueryAsync(cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override object? ExecuteScalar() + { + _connection.Open(); + + try + { + return _wrappedBatch.ExecuteScalar(); + } + finally + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + public override async Task ExecuteScalarAsync(CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedBatch.ExecuteScalarAsync(cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + // Also, refrain from bubbling up the close exception even if there's no original exception, + // since it's not relevant to the user - execution did complete successfully, and the connection + // close is just an internal detail that shouldn't cause user code to fail. + } + } + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + _connection.Open(); + + try + { + return _wrappedBatch.ExecuteReader(behavior | CommandBehavior.CloseConnection); + } + catch + { + try + { + _connection.Close(); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + } + + throw; + } + } + + protected override async Task ExecuteDbDataReaderAsync( + CommandBehavior behavior, + CancellationToken cancellationToken) + { + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + try + { + return await _wrappedBatch.ExecuteReaderAsync( + behavior | CommandBehavior.CloseConnection, + cancellationToken) + .ConfigureAwait(false); + } + catch + { + try + { + await _connection.CloseAsync().ConfigureAwait(false); + } + catch (Exception e) + { + ExceptionBuilder.TraceExceptionWithoutRethrow(e); + + // Swallow to allow the original exception to bubble up + } + + throw; + } + } + + protected override DbBatchCommand CreateDbBatchCommand() => throw new NotImplementedException(); + + public override void Cancel() + => _wrappedBatch.Cancel(); + + protected override DbBatchCommandCollection DbBatchCommands => _wrappedBatch.BatchCommands; + + public override int Timeout + { + get => _wrappedBatch.Timeout; + set => _wrappedBatch.Timeout = value; + } + + public override void Dispose() + { + var connection = _wrappedBatch.Connection; + + _wrappedBatch.Dispose(); + connection!.Dispose(); + } + + public override async ValueTask DisposeAsync() + { + var connection = _wrappedBatch.Connection; + + await _wrappedBatch.DisposeAsync().ConfigureAwait(false); + await connection!.DisposeAsync().ConfigureAwait(false); + } + + // In most case, preparation doesn't make sense on a connectionless command since prepared statements are + // usually bound to specific physical connections. + // When prepared statements are global (not bound to a specific connection), providers would need to + // provide their own connection-less implementation anyway (i.e. interacting with the originating + // DbDataSource), so they'd have to override this in any case. + public override void Prepare() + => throw ExceptionBuilder.NotSupportedOnDataSourceCommand(); + + public override Task PrepareAsync(CancellationToken cancellationToken = default) + => Task.FromException(ExceptionBuilder.NotSupportedOnDataSourceCommand()); + + // The below are incompatible with batches executed directly against DbDataSource, since no DbConnection + // is involved at the user API level and the DbBatchWrapper owns the DbConnection. + protected override DbConnection? DbConnection + { + get => throw ExceptionBuilder.NotSupportedOnDataSourceBatch(); + set => throw ExceptionBuilder.NotSupportedOnDataSourceBatch(); + } + + protected override DbTransaction? DbTransaction + { + get => throw ExceptionBuilder.NotSupportedOnDataSourceBatch(); + set => throw ExceptionBuilder.NotSupportedOnDataSourceBatch(); + } + } + } +} diff --git a/src/libraries/System.Data.Common/src/System/Data/Common/DbProviderFactory.cs b/src/libraries/System.Data.Common/src/System/Data/Common/DbProviderFactory.cs index 7282338b5407a..f133e5a0cddbf 100644 --- a/src/libraries/System.Data.Common/src/System/Data/Common/DbProviderFactory.cs +++ b/src/libraries/System.Data.Common/src/System/Data/Common/DbProviderFactory.cs @@ -66,5 +66,8 @@ public virtual bool CanCreateCommandBuilder public virtual DbParameter? CreateParameter() => null; public virtual DbDataSourceEnumerator? CreateDataSourceEnumerator() => null; + + public virtual DbDataSource CreateDataSource(string connectionString) + => new DefaultDataSource(this, connectionString); } } diff --git a/src/libraries/System.Data.Common/src/System/Data/Common/DefaultDataSource.cs b/src/libraries/System.Data.Common/src/System/Data/Common/DefaultDataSource.cs new file mode 100644 index 0000000000000..2829fb1b5aa9f --- /dev/null +++ b/src/libraries/System.Data.Common/src/System/Data/Common/DefaultDataSource.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Data.Common +{ + internal sealed class DefaultDataSource : DbDataSource + { + private readonly DbProviderFactory _dbProviderFactory; + private readonly string _connectionString; + + internal DefaultDataSource(DbProviderFactory dbProviderFactory, string connectionString) + { + _dbProviderFactory = dbProviderFactory; + _connectionString = connectionString; + } + + public override string ConnectionString => _connectionString; + + protected override DbConnection CreateDbConnection() + { + var connection = _dbProviderFactory.CreateConnection(); + if (connection is null) + { + throw new InvalidOperationException("DbProviderFactory returned a null connection"); + } + + connection.ConnectionString = _connectionString; + + return connection; + } + } +} diff --git a/src/libraries/System.Data.Common/src/System/Data/DataException.cs b/src/libraries/System.Data.Common/src/System/Data/DataException.cs index 7696f42c3db55..ffc9b5c9682a2 100644 --- a/src/libraries/System.Data.Common/src/System/Data/DataException.cs +++ b/src/libraries/System.Data.Common/src/System/Data/DataException.cs @@ -352,6 +352,17 @@ private static void ThrowDataException(string error, Exception? innerException) public static Exception ArgumentContainsNull(string paramName) => _Argument(paramName, SR.Format(SR.Data_ArgumentContainsNull, paramName)); public static Exception TypeNotAllowed(Type type) => _InvalidOperation(SR.Format(SR.Data_TypeNotAllowed, type.AssemblyQualifiedName)); + // + // Batch + // + + public static Exception NotSupportedOnDataSourceBatch() => Common.ADP.NotSupported(SR.Batch_NotSupportedOnDataSourceBatch); + + // + // Command + // + + public static Exception NotSupportedOnDataSourceCommand() => Common.ADP.NotSupported(SR.Command_NotSupportedOnDataSourceCommand); // // Collections