From ea79b63af9b27ebbb7a7f89041a7dd9644ec3d12 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sun, 1 Jan 2023 15:15:32 +0000 Subject: [PATCH] Allow transfer of ownership of DbConnection from application to DbContext Fixes #24199 --- .../RelationalDatabaseFacadeExtensions.cs | 12 ++- .../RelationalOptionsExtension.cs | 19 ++++ .../Storage/IRelationalConnection.cs | 16 +++ .../Storage/RelationalConnection.cs | 28 +++--- ...ServerDbContextOptionsBuilderExtensions.cs | 67 ++++++++++++- ...SqliteDbContextOptionsBuilderExtensions.cs | 63 +++++++++++- .../RelationalConnectionTest.cs | 63 ++++++++++++ .../RelationalEventIdTest.cs | 3 + .../RelationalOptionsExtensionTest.cs | 15 +++ .../ConnectionSpecificationTest.cs | 98 ++++++++++++++++--- ...SqlServerDbContextOptionsExtensionsTest.cs | 32 ++++++ .../Query/BadDataSqliteTest.cs | 3 + ...teDbContextOptionsBuilderExtensionsTest.cs | 32 ++++++ 13 files changed, 420 insertions(+), 31 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 0b52321320e..8e4c788ad04 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -646,16 +646,18 @@ public static DbConnection GetDbConnection(this DatabaseFacade databaseFacade) /// The connection can only be set when the existing connection, if any, is not open. /// /// - /// Note that the given connection must be disposed by application code since it was not created by Entity Framework. - /// - /// /// See Connections and connection strings for more information and examples. /// /// /// The for the context. /// The connection. - public static void SetDbConnection(this DatabaseFacade databaseFacade, DbConnection? connection) - => GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection = connection; + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. The default value is . + /// + public static void SetDbConnection(this DatabaseFacade databaseFacade, DbConnection? connection, bool contextOwnsConnection = false) + => GetFacadeDependencies(databaseFacade).RelationalConnection.SetDbConnection(connection, contextOwnsConnection); /// /// Gets the underlying connection string configured for this . diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index 1e3d542f2f3..bb855c34be8 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -25,6 +25,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension private string? _connectionString; private DbConnection? _connection; + private bool _connectionOwned; private int? _commandTimeout; private int? _maxBatchSize; private int? _minBatchSize; @@ -50,6 +51,7 @@ protected RelationalOptionsExtension(RelationalOptionsExtension copyFrom) { _connectionString = copyFrom._connectionString; _connection = copyFrom._connection; + _connectionOwned = copyFrom._connectionOwned; _commandTimeout = copyFrom._commandTimeout; _maxBatchSize = copyFrom._maxBatchSize; _minBatchSize = copyFrom._minBatchSize; @@ -101,6 +103,12 @@ public virtual RelationalOptionsExtension WithConnectionString(string? connectio public virtual DbConnection? Connection => _connection; + /// + /// if the is owned by the context and should be disposed appropriately. + /// + public virtual bool ConnectionOwned + => _connectionOwned; + /// /// Creates a new instance with all options the same as for this instance, but with the given option changed. /// It is unusual to call this method directly. Instead use . @@ -108,10 +116,21 @@ public virtual DbConnection? Connection /// The option to change. /// A new instance with the option changed. public virtual RelationalOptionsExtension WithConnection(DbConnection? connection) + => WithConnection(connection, owned: false); + + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// If , then the connection will become owned by the context, and will be disposed in the same way that a connection created by the context is disposed. + /// A new instance with the option changed. + public virtual RelationalOptionsExtension WithConnection(DbConnection? connection, bool owned) { var clone = Clone(); clone._connection = connection; + clone._connectionOwned = owned; return clone; } diff --git a/src/EFCore.Relational/Storage/IRelationalConnection.cs b/src/EFCore.Relational/Storage/IRelationalConnection.cs index eefa98bcb08..a1ad8324faf 100644 --- a/src/EFCore.Relational/Storage/IRelationalConnection.cs +++ b/src/EFCore.Relational/Storage/IRelationalConnection.cs @@ -47,6 +47,22 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa [AllowNull] DbConnection DbConnection { get; set; } + /// + /// Sets the underlying used to connect to the database. + /// + /// The connection object. + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// + /// + /// The connection can only be changed when the existing connection, if any, is not open. + /// + /// + void SetDbConnection(DbConnection? value, bool contextOwnsConnection); + /// /// The currently in use, or if not known. /// diff --git a/src/EFCore.Relational/Storage/RelationalConnection.cs b/src/EFCore.Relational/Storage/RelationalConnection.cs index b3675d3698d..17573633982 100644 --- a/src/EFCore.Relational/Storage/RelationalConnection.cs +++ b/src/EFCore.Relational/Storage/RelationalConnection.cs @@ -67,7 +67,7 @@ protected RelationalConnection(RelationalConnectionDependencies dependencies) if (relationalOptions.Connection != null) { _connection = relationalOptions.Connection; - _connectionOwned = false; + _connectionOwned = relationalOptions.ConnectionOwned; if (_connectionString != null) { @@ -172,19 +172,25 @@ public virtual DbConnection DbConnection } set { - if (!ReferenceEquals(_connection, value)) + SetDbConnection(value, contextOwnsConnection: false); + } + } + + /// + public virtual void SetDbConnection(DbConnection? value, bool contextOwnsConnection) + { + if (!ReferenceEquals(_connection, value)) + { + if (_openedCount > 0) { - if (_openedCount > 0) - { - throw new InvalidOperationException(RelationalStrings.CannotChangeWhenOpen); - } + throw new InvalidOperationException(RelationalStrings.CannotChangeWhenOpen); + } - Dispose(); + Dispose(); - _connection = value; - _connectionString = null; - _connectionOwned = false; - } + _connection = value; + _connectionString = null; + _connectionOwned = contextOwnsConnection; } } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs index 5f9cd9859a0..12414e7f8fb 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs @@ -75,6 +75,30 @@ public static DbContextOptionsBuilder UseSqlServer( return optionsBuilder; } + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to connect to a Microsoft SQL Server database. + /// + /// + /// See Using DbContextOptions, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for itd disposal. + /// + /// An optional action to allow additional SQL Server specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlServer( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + Action? sqlServerOptionsAction = null) + => UseSqlServer(optionsBuilder, connection, false, sqlServerOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 /// /// Configures the context to connect to a Microsoft SQL Server database. @@ -90,16 +114,22 @@ public static DbContextOptionsBuilder UseSqlServer( /// in the open state then EF will not open or close the connection. If the connection is in the closed /// state then EF will open and close the connection as needed. /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// /// An optional action to allow additional SQL Server specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseSqlServer( this DbContextOptionsBuilder optionsBuilder, DbConnection connection, + bool contextOwnsConnection, Action? sqlServerOptionsAction = null) { Check.NotNull(connection, nameof(connection)); - var extension = (SqlServerOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection); + var extension = (SqlServerOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection, contextOwnsConnection); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); ConfigureWarnings(optionsBuilder); @@ -170,7 +200,8 @@ public static DbContextOptionsBuilder UseSqlServer( /// /// An existing to be used to connect to the database. If the connection is /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for itd disposal. /// /// An optional action to allow additional SQL Server specific configuration. /// The options builder so that further configuration can be chained. @@ -182,6 +213,38 @@ public static DbContextOptionsBuilder UseSqlServer( => (DbContextOptionsBuilder)UseSqlServer( (DbContextOptionsBuilder)optionsBuilder, connection, sqlServerOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to connect to a Microsoft SQL Server database. + /// + /// + /// See Using DbContextOptions, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The type of context to be configured. + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// An optional action to allow additional SQL Server specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlServer( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + Action? sqlServerOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSqlServer( + (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, sqlServerOptionsAction); + private static SqlServerOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.Options.FindExtension() ?? new SqlServerOptionsExtension(); diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs index e3563c62da3..008899fbc68 100644 --- a/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs @@ -72,6 +72,28 @@ public static DbContextOptionsBuilder UseSqlite( return optionsBuilder; } + /// + /// Configures the context to connect to a SQLite database. + /// + /// + /// See Using DbContextOptions, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for itd disposal. + /// + /// An optional action to allow additional SQLite specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlite( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + Action? sqliteOptionsAction = null) + => UseSqlite(optionsBuilder, connection, false, sqliteOptionsAction); + /// /// Configures the context to connect to a SQLite database. /// @@ -85,16 +107,22 @@ public static DbContextOptionsBuilder UseSqlite( /// in the open state then EF will not open or close the connection. If the connection is in the closed /// state then EF will open and close the connection as needed. /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// /// An optional action to allow additional SQLite specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseSqlite( this DbContextOptionsBuilder optionsBuilder, DbConnection connection, + bool contextOwnsConnection, Action? sqliteOptionsAction = null) { Check.NotNull(connection, nameof(connection)); - var extension = (SqliteOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection); + var extension = (SqliteOptionsExtension)GetOrCreateExtension(optionsBuilder).WithConnection(connection, contextOwnsConnection); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); ConfigureWarnings(optionsBuilder); @@ -161,7 +189,8 @@ public static DbContextOptionsBuilder UseSqlite( /// /// An existing to be used to connect to the database. If the connection is /// in the open state then EF will not open or close the connection. If the connection is in the closed - /// state then EF will open and close the connection as needed. + /// state then EF will open and close the connection as needed. The caller owns the connection and is + /// responsible for itd disposal. /// /// An optional action to allow additional SQLite specific configuration. /// The options builder so that further configuration can be chained. @@ -173,6 +202,36 @@ public static DbContextOptionsBuilder UseSqlite( => (DbContextOptionsBuilder)UseSqlite( (DbContextOptionsBuilder)optionsBuilder, connection, sqliteOptionsAction); + /// + /// Configures the context to connect to a SQLite database. + /// + /// + /// See Using DbContextOptions, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The type of context to be configured. + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// An optional action to allow additional SQLite specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlite( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + Action? sqliteOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSqlite( + (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, sqliteOptionsAction); + private static SqliteOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder options) => options.Options.FindExtension() ?? new SqliteOptionsExtension(); diff --git a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs index 36003046036..463425dcc71 100644 --- a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs +++ b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs @@ -614,6 +614,69 @@ public void Existing_connection_is_not_disposed_even_after_being_opened_and_clos Assert.Equal(0, dbConnection.DisposeCount); } + [ConditionalFact] + public void Existing_connection_is_disposed_after_being_opened_and_closed_if_owned() + { + var dbConnection = new FakeDbConnection("Database=FrodoLives"); + var connection = new FakeRelationalConnection( + CreateOptions(new FakeRelationalOptionsExtension().WithConnection(dbConnection, owned: true))); + + Assert.Equal(0, connection.DbConnections.Count); + Assert.Same(dbConnection, connection.DbConnection); + + connection.Open(); + connection.Close(); + connection.Dispose(); + + Assert.Equal(1, dbConnection.OpenCount); + Assert.Equal(2, dbConnection.CloseCount); + Assert.Equal(1, dbConnection.DisposeCount); + + Assert.Equal(0, connection.DbConnections.Count); + } + + [ConditionalFact] + public void Existing_connection_is_disposed_if_owned_and_replaced() + { + var dbConnection1 = new FakeDbConnection("Database=FrodoLives"); + var connection = new FakeRelationalConnection( + CreateOptions(new FakeRelationalOptionsExtension().WithConnection(dbConnection1, owned: true))); + + Assert.Equal(0, connection.DbConnections.Count); + Assert.Same(dbConnection1, connection.DbConnection); + + Assert.Equal(0, dbConnection1.OpenCount); + Assert.Equal(0, dbConnection1.CloseCount); + Assert.Equal(0, dbConnection1.DisposeCount); + + Assert.Equal(0, connection.DbConnections.Count); + + var dbConnection2 = new FakeDbConnection("Database=FrodoLives"); + connection.SetDbConnection(dbConnection2, contextOwnsConnection: true); + + Assert.Equal(0, dbConnection1.OpenCount); + Assert.Equal(1, dbConnection1.CloseCount); + Assert.Equal(1, dbConnection1.DisposeCount); + + Assert.Equal(0, dbConnection2.OpenCount); + Assert.Equal(0, dbConnection2.CloseCount); + Assert.Equal(0, dbConnection2.DisposeCount); + + Assert.Equal(0, connection.DbConnections.Count); + + connection.Dispose(); + + Assert.Equal(0, dbConnection1.OpenCount); + Assert.Equal(1, dbConnection1.CloseCount); + Assert.Equal(1, dbConnection1.DisposeCount); + + Assert.Equal(0, dbConnection2.OpenCount); + Assert.Equal(1, dbConnection2.CloseCount); + Assert.Equal(1, dbConnection2.DisposeCount); + + Assert.Equal(0, connection.DbConnections.Count); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] diff --git a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs index 470afda0018..119833c34d7 100644 --- a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs @@ -171,6 +171,9 @@ private class FakeRelationalConnection : IRelationalConnection public DbConnection DbConnection { get; set; } = new FakeDbConnection(); + public void SetDbConnection(DbConnection value, bool contextOwnsConnection) + => throw new NotImplementedException(); + public DbContext Context => null; diff --git a/test/EFCore.Relational.Tests/RelationalOptionsExtensionTest.cs b/test/EFCore.Relational.Tests/RelationalOptionsExtensionTest.cs index 420a3dfbb58..5b53c732cca 100644 --- a/test/EFCore.Relational.Tests/RelationalOptionsExtensionTest.cs +++ b/test/EFCore.Relational.Tests/RelationalOptionsExtensionTest.cs @@ -21,6 +21,21 @@ public void Can_set_Connection() optionsExtension = (FakeRelationalOptionsExtension)optionsExtension.WithConnection(connection); Assert.Same(connection, optionsExtension.Connection); + Assert.False(optionsExtension.ConnectionOwned); + } + + [ConditionalFact] + public void Can_set_owned_Connection() + { + var optionsExtension = new FakeRelationalOptionsExtension(); + + Assert.Null(optionsExtension.Connection); + + var connection = new FakeDbConnection("A=B"); + optionsExtension = (FakeRelationalOptionsExtension)optionsExtension.WithConnection(connection, owned: true); + + Assert.Same(connection, optionsExtension.Connection); + Assert.True(optionsExtension.ConnectionOwned); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs index 044879b4323..49832803ca2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs @@ -97,38 +97,68 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) .UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString, b => b.ApplyConfiguration()); } - [ConditionalFact] - public void Can_specify_no_connection_in_OnConfiguring() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_specify_no_connection_in_OnConfiguring(bool contextOwnsConnection) { var serviceProvider = new ServiceCollection() .AddScoped(p => new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString)) .AddDbContext().BuildServiceProvider(validateScopes: true); + SqlConnection connection; + using (SqlServerTestStore.GetNorthwindStore()) { using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); - context.Database.SetDbConnection(connection); + connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + context.Database.SetDbConnection(connection, contextOwnsConnection); Assert.True(context.Customers.Any()); } + + if (contextOwnsConnection) + { + Assert.Throws(() => connection.Open()); // Disposed + } + else + { + connection.Open(); + connection.Close(); + connection.Dispose(); + } } - [ConditionalFact] - public void Can_specify_no_connection_in_OnConfiguring_with_default_service_provider() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_specify_no_connection_in_OnConfiguring_with_default_service_provider(bool contextOwnsConnection) { + SqlConnection connection; + using (SqlServerTestStore.GetNorthwindStore()) { using var context = new NoneInOnConfiguringContext(); - using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); - context.Database.SetDbConnection(connection); + connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + context.Database.SetDbConnection(connection, contextOwnsConnection); Assert.True(context.Customers.Any()); } + + if (contextOwnsConnection) + { + Assert.Throws(() => connection.Open()); // Disposed + } + else + { + connection.Open(); + connection.Close(); + connection.Dispose(); + } } [ConditionalFact] @@ -159,6 +189,44 @@ public void Can_specify_connection_in_OnConfiguring_with_default_service_provide } } + [ConditionalFact] + public void Can_specify_owned_connection_in_OnConfiguring() + { + var serviceProvider + = new ServiceCollection() + .AddSingleton(_ => new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString)) + .AddDbContext().BuildServiceProvider(validateScopes: true); + + SqlConnection connection; + + using (SqlServerTestStore.GetNorthwindStore()) + { + connection = serviceProvider.GetRequiredService(); + + using var scope = serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + Assert.True(context.Customers.Any()); + } + + Assert.Throws(() => connection.Open()); // Disposed + } + + [ConditionalFact] + public void Can_specify_owned_connection_in_OnConfiguring_with_default_service_provider() + { + SqlConnection connection; + + using (SqlServerTestStore.GetNorthwindStore()) + { + connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + using var context = new OwnedConnectionInOnConfiguringContext(connection); + + Assert.True(context.Customers.Any()); + } + + Assert.Throws(() => connection.Open()); // Disposed + } + [ConditionalFact] public void Can_specify_then_change_connection() { @@ -233,11 +301,19 @@ public override void Dispose() } } - // ReSharper disable once UnusedMember.Local - private class StringInConfigContext : NorthwindContextBase + private class OwnedConnectionInOnConfiguringContext : NorthwindContextBase { + private readonly SqlConnection _connection; + + public OwnedConnectionInOnConfiguringContext(SqlConnection connection) + { + _connection = connection; + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseSqlServer("Database=Crunchie", b => b.ApplyConfiguration()); + => optionsBuilder + .EnableServiceProviderCaching(false) + .UseSqlServer(_connection, contextOwnsConnection: true, b => b.ApplyConfiguration()); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.Tests/SqlServerDbContextOptionsExtensionsTest.cs b/test/EFCore.SqlServer.Tests/SqlServerDbContextOptionsExtensionsTest.cs index bd8d9d91702..4205a2413c4 100644 --- a/test/EFCore.SqlServer.Tests/SqlServerDbContextOptionsExtensionsTest.cs +++ b/test/EFCore.SqlServer.Tests/SqlServerDbContextOptionsExtensionsTest.cs @@ -68,6 +68,22 @@ public void Can_add_extension_with_connection() var extension = optionsBuilder.Options.Extensions.OfType().Single(); Assert.Same(connection, extension.Connection); + Assert.False(extension.ConnectionOwned); + Assert.Null(extension.ConnectionString); + } + + [ConditionalFact] + public void Can_add_extension_with_owned_connection() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new SqlConnection(); + + optionsBuilder.UseSqlServer(connection, contextOwnsConnection: true); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.True(extension.ConnectionOwned); Assert.Null(extension.ConnectionString); } @@ -82,6 +98,22 @@ public void Can_add_extension_with_connection_using_generic_options() var extension = optionsBuilder.Options.Extensions.OfType().Single(); Assert.Same(connection, extension.Connection); + Assert.False(extension.ConnectionOwned); + Assert.Null(extension.ConnectionString); + } + + [ConditionalFact] + public void Can_add_extension_with_owned_connection_using_generic_options() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new SqlConnection(); + + optionsBuilder.UseSqlServer(connection, contextOwnsConnection: true); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.True(extension.ConnectionOwned); Assert.Null(extension.ConnectionString); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs index ff233ab658b..7d741380e64 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs @@ -340,6 +340,9 @@ public IDbContextTransaction CurrentTransaction public string ConnectionString { get; set; } public DbConnection DbConnection { get; set; } = new SqliteConnection(); + public void SetDbConnection(DbConnection value, bool contextOwnsConnection) + => throw new NotImplementedException(); + public DbContext Context => null; diff --git a/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs b/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs index bc319faa7ee..4480a4b29c2 100644 --- a/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs +++ b/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs @@ -67,6 +67,22 @@ public void Can_add_extension_with_connection() var extension = optionsBuilder.Options.Extensions.OfType().Single(); Assert.Same(connection, extension.Connection); + Assert.False(extension.ConnectionOwned); + Assert.Null(extension.ConnectionString); + } + + [ConditionalFact] + public void Can_add_extension_with_owned_connection() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new SqliteConnection(); + + optionsBuilder.UseSqlite(connection, contextOwnsConnection: true); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.True(extension.ConnectionOwned); Assert.Null(extension.ConnectionString); } @@ -94,6 +110,22 @@ public void Can_add_extension_with_connection_using_generic_options() var extension = optionsBuilder.Options.Extensions.OfType().Single(); Assert.Same(connection, extension.Connection); + Assert.False(extension.ConnectionOwned); + Assert.Null(extension.ConnectionString); + } + + [ConditionalFact] + public void Can_add_owned_extension_with_connection_using_generic_options() + { + var optionsBuilder = new DbContextOptionsBuilder(); + var connection = new SqliteConnection(); + + optionsBuilder.UseSqlite(connection, contextOwnsConnection: true); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Same(connection, extension.Connection); + Assert.True(extension.ConnectionOwned); Assert.Null(extension.ConnectionString); }