From 0e31df47a83b3157e5af028477631e54defb524f Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 20 Dec 2019 14:11:38 -0800 Subject: [PATCH] Allow DbConnection or connection string to be changed on existing DbContext Fixes #6525 (Allow connections string to be set/changed) Fixes #8494 (Allow connection to be set/changed) Fixes #8427 (Parameterless overloads of UseSqlServer, UseSqlite) --- .../RelationalDatabaseFacadeExtensions.cs | 37 ++++ .../Properties/RelationalStrings.Designer.cs | 8 +- .../Properties/RelationalStrings.resx | 3 + .../Storage/IRelationalConnection.cs | 33 +++- .../Storage/RelationalConnection.cs | 100 ++++++++-- .../Internal/SqlServerGeometryTypeMapping.cs | 2 +- ...ServerDbContextOptionsBuilderExtensions.cs | 50 +++++ .../Internal/SqlServerDatabaseModelFactory.cs | 2 +- .../SqlServerRetryingExecutionStrategy.cs | 2 +- .../Storage/Internal/SqlServerConnection.cs | 6 +- .../Internal/SqlServerDatabaseCreator.cs | 2 +- .../Internal/SqlServerDateTimeTypeMapping.cs | 2 +- .../Internal/SqlServerTimeSpanTypeMapping.cs | 2 +- .../SqlServerTransientExceptionDetector.cs | 2 +- ...SqliteDbContextOptionsBuilderExtensions.cs | 50 +++++ .../Storage/Internal/SqliteDatabaseCreator.cs | 2 +- .../Internal/SqliteRelationalConnection.cs | 4 +- .../TwoDatabasesTestBase.cs | 175 ++++++++++++++++++ .../RelationalConnectionTest.cs | 107 +++++++++-- .../ConnectionSpecificationTest.cs | 147 ++++++++++++++- .../SqlServerEndToEndTest.cs | 1 - .../TwoDatabasesSqlServerTest.cs | 29 +++ .../TwoDatabasesSqliteTest.cs | 34 ++++ ...teDbContextOptionsBuilderExtensionsTest.cs | 13 ++ 24 files changed, 761 insertions(+), 52 deletions(-) create mode 100644 test/EFCore.Relational.Specification.Tests/TwoDatabasesTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/TwoDatabasesSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/TwoDatabasesSqliteTest.cs diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 25265deaa39..8609eb690d6 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -385,6 +385,43 @@ public static async Task ExecuteSqlRawAsync( public static DbConnection GetDbConnection([NotNull] this DatabaseFacade databaseFacade) => GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection; + /// + /// + /// Sets the underlying ADO.NET for this . + /// + /// + /// 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. + /// + /// + /// The for the context. + /// The connection. + public static void SetDbConnection([NotNull] this DatabaseFacade databaseFacade, [CanBeNull] DbConnection connection) + => GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection = connection; + + /// + /// Gets the underlying connection string configured for this . + /// + /// The for the context. + /// The connection string. + public static string GetConnectionString([NotNull] this DatabaseFacade databaseFacade) + => GetFacadeDependencies(databaseFacade).RelationalConnection.ConnectionString; + + /// + /// + /// Sets the underlying connection string configured for this . + /// + /// + /// It may not be possible to change the connection string if existing connection, if any, is open. + /// + /// + /// The for the context. + /// The connection string. + public static void SetConnectionString([NotNull] this DatabaseFacade databaseFacade, [CanBeNull] string connectionString) + => GetFacadeDependencies(databaseFacade).RelationalConnection.ConnectionString = connectionString; + /// /// Opens the underlying . /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index c99863a30cf..316be0dac26 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -38,6 +38,12 @@ public static string ModificationCommandInvalidEntityState([CanBeNull] object en public static string NoDbCommand => GetString("NoDbCommand"); + /// + /// The 'DbConnection' is currently in use. The connection can only be changed when the existing connection is not being used. + /// + public static string CannotChangeWhenOpen + => GetString("CannotChangeWhenOpen"); + /// /// Database operation expected to affect {expectedRows} row(s) but actually affected {actualRows} row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index eceab20d10a..df2095d5dc5 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -123,6 +123,9 @@ Cannot create a 'DbCommand' for a non-relational query. + + The 'DbConnection' is currently in use. The connection can only be changed when the existing connection is not being used. + Database operation expected to affect {expectedRows} row(s) but actually affected {actualRows} row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions. diff --git a/src/EFCore.Relational/Storage/IRelationalConnection.cs b/src/EFCore.Relational/Storage/IRelationalConnection.cs index 29f96bf65c3..fcf12d41fa4 100644 --- a/src/EFCore.Relational/Storage/IRelationalConnection.cs +++ b/src/EFCore.Relational/Storage/IRelationalConnection.cs @@ -5,6 +5,7 @@ using System.Data.Common; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore.Storage @@ -27,14 +28,37 @@ namespace Microsoft.EntityFrameworkCore.Storage public interface IRelationalConnection : IRelationalTransactionManager, IDisposable, IAsyncDisposable { /// - /// Gets the connection string for the database. + /// Gets or sets the connection string for the database. /// - string ConnectionString { get; } + string ConnectionString + { + get => throw new NotImplementedException(); + [param: CanBeNull] set => throw new NotImplementedException(); + } /// - /// Gets the underlying used to connect to the database. + /// Returns the configured connection string only if it has been set or a valid exists. /// - DbConnection DbConnection { get; } + /// The connection string. + /// when connection string cannot be obtained. + string GetCheckedConnectionString() => ConnectionString; + + /// + /// + /// Gets or sets the underlying used to connect to the database. + /// + /// + /// The connection can only be changed when the existing connection, if any, is not open. + /// + /// + /// Note that the connection must be disposed by application code since it was not created by Entity Framework. + /// + /// + DbConnection DbConnection + { + get => throw new NotImplementedException(); + [param: CanBeNull] set => throw new NotImplementedException(); + } /// /// The currently in use, or null if not known. @@ -97,6 +121,7 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa /// /// The semaphore. /// + [Obsolete("EF Core no longer uses this semaphore. It will be removed in an upcoming release.")] SemaphoreSlim Semaphore { get; } } } diff --git a/src/EFCore.Relational/Storage/RelationalConnection.cs b/src/EFCore.Relational/Storage/RelationalConnection.cs index 99cb6454883..d9c5cfe4d67 100644 --- a/src/EFCore.Relational/Storage/RelationalConnection.cs +++ b/src/EFCore.Relational/Storage/RelationalConnection.cs @@ -34,8 +34,8 @@ namespace Microsoft.EntityFrameworkCore.Storage /// public abstract class RelationalConnection : IRelationalConnection, ITransactionEnlistmentManager { - private readonly string _connectionString; - private readonly bool _connectionOwned; + private string _connectionString; + private bool _connectionOwned; private int _openedCount; private bool _openedInternally; private int? _commandTimeout; @@ -58,24 +58,23 @@ protected RelationalConnection([NotNull] RelationalConnectionDependencies depend _commandTimeout = relationalOptions.CommandTimeout; + _connectionString = string.IsNullOrWhiteSpace(relationalOptions.ConnectionString) + ? null + : dependencies.ConnectionStringResolver.ResolveConnectionString(relationalOptions.ConnectionString); + if (relationalOptions.Connection != null) { - if (!string.IsNullOrWhiteSpace(relationalOptions.ConnectionString)) - { - throw new InvalidOperationException(RelationalStrings.ConnectionAndConnectionString); - } - _connection = relationalOptions.Connection; _connectionOwned = false; - } - else if (!string.IsNullOrWhiteSpace(relationalOptions.ConnectionString)) - { - _connectionString = dependencies.ConnectionStringResolver.ResolveConnectionString(relationalOptions.ConnectionString); - _connectionOwned = true; + + if (_connectionString != null) + { + _connection.ConnectionString = _connectionString; + } } else { - throw new InvalidOperationException(RelationalStrings.NoConnectionOrConnectionString); + _connectionOwned = true; } } @@ -101,15 +100,80 @@ protected RelationalConnection([NotNull] RelationalConnectionDependencies depend protected abstract DbConnection CreateDbConnection(); /// - /// Gets the connection string for the database. + /// Gets or sets the connection string for the database. /// - public virtual string ConnectionString => _connectionString ?? DbConnection.ConnectionString; + public virtual string ConnectionString + { + get => _connectionString ?? _connection?.ConnectionString; + set + { + if (_connection != null + && !string.Equals(_connection.ConnectionString, value, StringComparison.InvariantCulture)) + { + // Let ADO.NET throw if this is not valid for the state of the connection. + _connection.ConnectionString = value; + } + + _connectionString = value; + } + } /// - /// Gets the underlying used to connect to the database. + /// Returns the configured connection string only if it has been set or a valid exists. + /// + /// The connection string. + /// when connection string cannot be obtained. + public virtual string GetCheckedConnectionString() + { + var connectionString = ConnectionString; + if (connectionString == null) + { + throw new InvalidOperationException(RelationalStrings.NoConnectionOrConnectionString); + } + + return connectionString; + } + + /// + /// + /// Gets or sets the underlying used to connect to the database. + /// + /// + /// The connection can only be changed when the existing connection, if any, is not open. + /// + /// + /// Note that a connection set must be disposed by application code since it was not created by Entity Framework. + /// /// public virtual DbConnection DbConnection - => _connection ??= CreateDbConnection(); + { + get + { + if (_connection == null + && _connectionString == null) + { + throw new InvalidOperationException(RelationalStrings.NoConnectionOrConnectionString); + } + + return _connection ??= CreateDbConnection(); + } + set + { + if (!ReferenceEquals(_connection, value)) + { + if (_openedCount > 0) + { + throw new InvalidOperationException(RelationalStrings.CannotChangeWhenOpen); + } + + Dispose(); + + _connection = value; + _connectionString = null; + _connectionOwned = false; + } + } + } /// /// Gets the current transaction. @@ -737,6 +801,7 @@ private bool ShouldClose() /// /// The semaphore used to serialize access to this connection. /// + [Obsolete("EF Core no longer uses this semaphore. It will be removed in an upcoming release.")] public virtual SemaphoreSlim Semaphore { get; } = new SemaphoreSlim(1); private Transaction _enlistedTransaction; @@ -755,6 +820,7 @@ public virtual void Dispose() DbConnection.Dispose(); _connection = null; _openedCount = 0; + _openedInternally = false; } } diff --git a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs index c7b0c8353fe..4a07e76de1d 100644 --- a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs +++ b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs @@ -10,7 +10,7 @@ using System.Text; using System.Threading; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs index 19d380c0340..946dc05f8bc 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbContextOptionsBuilderExtensions.cs @@ -17,6 +17,35 @@ namespace Microsoft.EntityFrameworkCore /// public static class SqlServerDbContextOptionsExtensions { + /// + /// + /// Configures the context to connect to a Microsoft SQL Server database, but without initially setting any + /// or connection string. + /// + /// + /// The connection or connection string must be set before the is used to connect + /// to a database. Set a connection using . + /// Set a connection string using . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional SQL Server specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlServer( + [NotNull] this DbContextOptionsBuilder optionsBuilder, + [CanBeNull] Action sqlServerOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(GetOrCreateExtension(optionsBuilder)); + + ConfigureWarnings(optionsBuilder); + + sqlServerOptionsAction?.Invoke(new SqlServerDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + /// /// Configures the context to connect to a Microsoft SQL Server database. /// @@ -72,6 +101,27 @@ public static DbContextOptionsBuilder UseSqlServer( return optionsBuilder; } + /// + /// + /// Configures the context to connect to a Microsoft SQL Server database, but without initially setting any + /// or connection string. + /// + /// + /// The connection or connection string must be set before the is used to connect + /// to a database. Set a connection using . + /// Set a connection string using . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional SQL Server specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlServer( + [NotNull] this DbContextOptionsBuilder optionsBuilder, + [CanBeNull] Action sqlServerOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSqlServer( + (DbContextOptionsBuilder)optionsBuilder, sqlServerOptionsAction); + /// /// Configures the context to connect to a Microsoft SQL Server database. /// diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index 7816e497e57..bf0902e2fb7 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -10,7 +10,7 @@ using System.Text; using System.Text.RegularExpressions; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/EFCore.SqlServer/SqlServerRetryingExecutionStrategy.cs b/src/EFCore.SqlServer/SqlServerRetryingExecutionStrategy.cs index a46a62dfe53..7aee208d508 100644 --- a/src/EFCore.SqlServer/SqlServerRetryingExecutionStrategy.cs +++ b/src/EFCore.SqlServer/SqlServerRetryingExecutionStrategy.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage; diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerConnection.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerConnection.cs index 1f7b5935874..8fcac7520d2 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerConnection.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerConnection.cs @@ -3,7 +3,7 @@ using System.Data.Common; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; @@ -45,7 +45,7 @@ public SqlServerConnection([NotNull] RelationalConnectionDependencies dependenci /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override DbConnection CreateDbConnection() => new SqlConnection(ConnectionString); + protected override DbConnection CreateDbConnection() => new SqlConnection(GetCheckedConnectionString()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -55,7 +55,7 @@ public SqlServerConnection([NotNull] RelationalConnectionDependencies dependenci /// public virtual ISqlServerConnection CreateMasterConnection() { - var connectionStringBuilder = new SqlConnectionStringBuilder(ConnectionString) { InitialCatalog = "master" }; + var connectionStringBuilder = new SqlConnectionStringBuilder(GetCheckedConnectionString()) { InitialCatalog = "master" }; connectionStringBuilder.Remove("AttachDBFilename"); var contextOptions = new DbContextOptionsBuilder() diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs index 188223435a7..5fd7ecae890 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.Transactions; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.SqlServer.Internal; diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs index 843af09ec47..32a41f1d254 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs @@ -4,7 +4,7 @@ using System.Data; using System.Data.Common; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs index bae11c13446..9e2c1da6652 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs @@ -4,7 +4,7 @@ using System.Data; using System.Data.Common; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTransientExceptionDetector.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTransientExceptionDetector.cs index 1dd318a92e3..b8b89b4513f 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTransientExceptionDetector.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTransientExceptionDetector.cs @@ -3,7 +3,7 @@ using System; using JetBrains.Annotations; -using Microsoft.Data.SqlClient; // Note: Hard reference to SqlClient here. +using Microsoft.Data.SqlClient; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal { diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs index 09a73ab3fc5..16785d124e8 100644 --- a/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteDbContextOptionsBuilderExtensions.cs @@ -17,6 +17,35 @@ namespace Microsoft.EntityFrameworkCore /// public static class SqliteDbContextOptionsBuilderExtensions { + /// + /// + /// Configures the context to connect to a SQLite database, but without initially setting any + /// or connection string. + /// + /// + /// The connection or connection string must be set before the is used to connect + /// to a database. Set a connection using . + /// Set a connection string using . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional SQLite specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlite( + [NotNull] this DbContextOptionsBuilder optionsBuilder, + [CanBeNull] Action sqliteOptionsAction = null) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(GetOrCreateExtension(optionsBuilder)); + + ConfigureWarnings(optionsBuilder); + + sqliteOptionsAction?.Invoke(new SqliteDbContextOptionsBuilder(optionsBuilder)); + + return optionsBuilder; + } + /// /// Configures the context to connect to a SQLite database. /// @@ -71,6 +100,27 @@ public static DbContextOptionsBuilder UseSqlite( return optionsBuilder; } + /// + /// + /// Configures the context to connect to a SQLite database, but without initially setting any + /// or connection string. + /// + /// + /// The connection or connection string must be set before the is used to connect + /// to a database. Set a connection using . + /// Set a connection string using . + /// + /// + /// The builder being used to configure the context. + /// An optional action to allow additional SQLite specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseSqlite( + [NotNull] this DbContextOptionsBuilder optionsBuilder, + [CanBeNull] Action sqliteOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseSqlite( + (DbContextOptionsBuilder)optionsBuilder, sqliteOptionsAction); + /// /// Configures the context to connect to a SQLite database. /// diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDatabaseCreator.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDatabaseCreator.cs index 03076df737c..93582ce26fb 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDatabaseCreator.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDatabaseCreator.cs @@ -78,7 +78,7 @@ public override void Create() /// public override bool Exists() { - var connectionOptions = new SqliteConnectionStringBuilder(_connection.ConnectionString); + var connectionOptions = new SqliteConnectionStringBuilder(_connection.GetCheckedConnectionString()); if (connectionOptions.DataSource.Equals(":memory:", StringComparison.OrdinalIgnoreCase) || connectionOptions.Mode == SqliteOpenMode.Memory) { diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs index 0786b3bdaf4..e53affbbaa1 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteRelationalConnection.cs @@ -71,7 +71,7 @@ public SqliteRelationalConnection( /// protected override DbConnection CreateDbConnection() { - var connection = new SqliteConnection(ConnectionString); + var connection = new SqliteConnection(GetCheckedConnectionString()); if (_loadSpatialite) { @@ -89,7 +89,7 @@ protected override DbConnection CreateDbConnection() /// public virtual ISqliteRelationalConnection CreateReadOnlyConnection() { - var connectionStringBuilder = new SqliteConnectionStringBuilder(ConnectionString) { Mode = SqliteOpenMode.ReadOnly }; + var connectionStringBuilder = new SqliteConnectionStringBuilder(GetCheckedConnectionString()) { Mode = SqliteOpenMode.ReadOnly }; var contextOptions = new DbContextOptionsBuilder().UseSqlite(connectionStringBuilder.ToString()).Options; diff --git a/test/EFCore.Relational.Specification.Tests/TwoDatabasesTestBase.cs b/test/EFCore.Relational.Specification.Tests/TwoDatabasesTestBase.cs new file mode 100644 index 00000000000..8adc3f6889a --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TwoDatabasesTestBase.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations.Schema; +using System.Data.Common; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class TwoDatabasesTestBase + { + protected FixtureBase Fixture { get; } + + protected TwoDatabasesTestBase(FixtureBase fixture) + { + Fixture = fixture; + } + + [ConditionalFact] + public void Can_query_from_one_connection_string_and_save_changes_to_another() + { + using var context1 = CreateBackingContext("TwoDatabasesOne"); + using var context2 = CreateBackingContext("TwoDatabasesTwo"); + + Assert.NotEqual(context1.Database.GetConnectionString(), context2.Database.GetConnectionString()); + + context1.Database.EnsureCreatedResiliently(); + context2.Database.EnsureCreatedResiliently(); + + using (var context = new TwoDatabasesContext(CreateTestOptions(new DbContextOptionsBuilder()).Options)) + { + context.Database.SetConnectionString(context1.Database.GetConnectionString()); + + var data = context.Foos.ToList(); + data[0].Bar = "Modified One"; + data[1].Bar = "Modified Two"; + + context.Database.SetConnectionString(context2.Database.GetConnectionString()); + + context.SaveChanges(); + } + + Assert.Equal(new[] { "One", "Two" }, context1.Foos.Select(e => e.Bar).ToList()); + Assert.Equal(new[] { "Modified One", "Modified Two" }, context2.Foos.Select(e => e.Bar).ToList()); + } + + [ConditionalFact] + public void Can_query_from_one_connection_and_save_changes_to_another() + { + using var context1 = CreateBackingContext("TwoDatabasesOneB"); + using var context2 = CreateBackingContext("TwoDatabasesTwoB"); + + Assert.NotSame(context1.Database.GetDbConnection(), context2.Database.GetDbConnection()); + + context1.Database.EnsureCreatedResiliently(); + context2.Database.EnsureCreatedResiliently(); + + using (var context = new TwoDatabasesContext(CreateTestOptions(new DbContextOptionsBuilder()).Options)) + { + context.Database.SetDbConnection(context1.Database.GetDbConnection()); + + var data = context.Foos.ToList(); + data[0].Bar = "Modified One"; + data[1].Bar = "Modified Two"; + + context.Database.SetDbConnection(context2.Database.GetDbConnection()); + + context.SaveChanges(); + } + + Assert.Equal(new[] { "One", "Two" }, context1.Foos.Select(e => e.Bar).ToList()); + Assert.Equal(new[] { "Modified One", "Modified Two" }, context2.Foos.Select(e => e.Bar).ToList()); + } + + [ConditionalFact] + public void Can_set_connection_string_in_interceptor() + { + using var context1 = CreateBackingContext("TwoDatabasesIntercept"); + + context1.Database.EnsureCreatedResiliently(); + + using (var context = new TwoDatabasesContext( + CreateTestOptions(new DbContextOptionsBuilder(), withConnectionString: true) + .AddInterceptors(new ConnectionStringConnectionInterceptor( + context1.Database.GetConnectionString(), DummyConnectionString)) + .Options)) + { + var data = context.Foos.ToList(); + data[0].Bar = "Modified One"; + data[1].Bar = "Modified Two"; + + context.SaveChanges(); + } + + Assert.Equal(new[] { "Modified One", "Modified Two" }, context1.Foos.Select(e => e.Bar).ToList()); + } + + protected class ConnectionStringConnectionInterceptor : DbConnectionInterceptor + { + private readonly string _goodConnectionString; + private readonly string _dummyConnectionString; + + public ConnectionStringConnectionInterceptor(string goodConnectionString, string dummyConnectionString) + { + _goodConnectionString = goodConnectionString; + _dummyConnectionString = dummyConnectionString; + } + + public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) + { + Assert.Equal(_dummyConnectionString, eventData.Context.Database.GetConnectionString()); + eventData.Context.Database.SetConnectionString(_goodConnectionString); + + return result; + } + + public override void ConnectionClosed(DbConnection connection, ConnectionEndEventData eventData) + { + Assert.Equal(_goodConnectionString, eventData.Context.Database.GetConnectionString()); + eventData.Context.Database.SetConnectionString(_dummyConnectionString); + } + } + + protected abstract DbContextOptionsBuilder CreateTestOptions( + DbContextOptionsBuilder optionsBuilder, bool withConnectionString = false); + + protected abstract TwoDatabasesWithDataContext CreateBackingContext(string databaseName); + + protected abstract string DummyConnectionString { get; } + + protected class TwoDatabasesContext : DbContext + { + public TwoDatabasesContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + + public IQueryable Foos => this.Set().OrderBy(e => e.Id); + } + + protected class TwoDatabasesWithDataContext : TwoDatabasesContext + { + public TwoDatabasesWithDataContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder + .Entity() + .HasData( + new Foo { Id = 1, Bar = "One" }, + new Foo { Id = 2, Bar = "Two" }); + } + } + + protected class Foo + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public string Bar { get; set; } + } + } +} diff --git a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs index 7885fcd9f79..87ecea52c90 100644 --- a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs +++ b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs @@ -137,6 +137,41 @@ public void Can_create_new_connection_lazily_using_given_connection_string() Assert.Equal("Database=FrodoLives", dbConnection.ConnectionString); } + [ConditionalFact] + public void Can_change_or_reset_connection_string() + { + using var connection = new FakeRelationalConnection( + CreateOptions(new FakeRelationalOptionsExtension().WithConnectionString("Database=FrodoLives"))); + + connection.ConnectionString = null; + Assert.Null(connection.ConnectionString); + + connection.ConnectionString = "Database=SamLives"; + Assert.Equal("Database=SamLives", connection.ConnectionString); + + Assert.Equal(0, connection.DbConnections.Count); + + var dbConnection = connection.DbConnection; + + Assert.Equal(1, connection.DbConnections.Count); + Assert.Equal("Database=SamLives", connection.ConnectionString); + Assert.Equal("Database=SamLives", dbConnection.ConnectionString); + + connection.ConnectionString = null; + + Assert.Equal(1, connection.DbConnections.Count); + Assert.Null(connection.ConnectionString); + Assert.Null(dbConnection.ConnectionString); + + connection.ConnectionString = "Database=MerryLives"; + + dbConnection = connection.DbConnection; + + Assert.Equal(1, connection.DbConnections.Count); + Assert.Equal("Database=MerryLives", connection.ConnectionString); + Assert.Equal("Database=MerryLives", dbConnection.ConnectionString); + } + [ConditionalFact] public void Lazy_connection_is_opened_and_closed_when_necessary() { @@ -439,6 +474,46 @@ public void Existing_connection_can_be_opened_and_closed_externally() Assert.Equal(2, dbConnection.CloseCount); } + [ConditionalFact] + public void Existing_connection_can_be_changed_and_reset() + { + var dbConnection = new FakeDbConnection("Database=FrodoLives"); + + using var connection = new FakeRelationalConnection( + CreateOptions(new FakeRelationalOptionsExtension().WithConnection(dbConnection))); + + Assert.Equal(0, connection.DbConnections.Count); + + connection.DbConnection = null; + Assert.Null(connection.ConnectionString); + + dbConnection = new FakeDbConnection("Database=SamLives"); + connection.DbConnection = dbConnection; + + Assert.Equal("Database=SamLives", connection.ConnectionString); + + Assert.Equal(0, connection.DbConnections.Count); + Assert.Same(dbConnection, connection.DbConnection); + Assert.Equal(0, connection.DbConnections.Count); + Assert.Equal("Database=SamLives", connection.ConnectionString); + + connection.DbConnection = null; + + Assert.Equal(0, connection.DbConnections.Count); + Assert.Null(connection.ConnectionString); + + connection.ConnectionString = "Database=MerryLives"; + + dbConnection = new FakeDbConnection("Database=MerryLives"); + connection.DbConnection = dbConnection; + + Assert.Equal(0, connection.DbConnections.Count); + Assert.Same(dbConnection, connection.DbConnection); + Assert.Equal(0, connection.DbConnections.Count); + Assert.Equal("Database=MerryLives", connection.ConnectionString); + + } + [ConditionalFact] public async Task Existing_connection_can_be_opened_and_closed_externally_async() { @@ -812,27 +887,33 @@ public void Throws_if_multiple_relational_stores_configured() } [ConditionalFact] - public void Throws_if_no_connection_or_connection_string_is_specified() + public void Throws_if_no_connection_or_connection_string_is_specified_only_when_accessed() { + var connection = new FakeRelationalConnection(CreateOptions(new FakeRelationalOptionsExtension())); + Assert.Equal( RelationalStrings.NoConnectionOrConnectionString, Assert.Throws( - () => new FakeRelationalConnection( - CreateOptions( - new FakeRelationalOptionsExtension()))).Message); + () => connection.DbConnection).Message); + + Assert.Null(connection.ConnectionString); + + Assert.Equal( + RelationalStrings.NoConnectionOrConnectionString, + Assert.Throws( + () => connection.GetCheckedConnectionString()).Message); } [ConditionalFact] - public void Throws_if_both_connection_and_connection_string_are_specified() + public void Puts_connection_string_on_connection_if_both_are_specified() { - Assert.Equal( - RelationalStrings.ConnectionAndConnectionString, - Assert.Throws( - () => new FakeRelationalConnection( - CreateOptions( - new FakeRelationalOptionsExtension() - .WithConnection(new FakeDbConnection("Database=FrodoLives")) - .WithConnectionString("Database=FrodoLives")))).Message); + var connection = new FakeRelationalConnection( + CreateOptions( + new FakeRelationalOptionsExtension() + .WithConnection(new FakeDbConnection("Database=FrodoLives")) + .WithConnectionString("Database=SamLives"))); + + Assert.Equal("Database=SamLives", connection.DbConnection.ConnectionString); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs index 52817bf9731..80074803b29 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConnectionSpecificationTest.cs @@ -18,6 +18,59 @@ namespace Microsoft.EntityFrameworkCore { public class ConnectionSpecificationTest { + [ConditionalFact] + public void Can_specify_no_connection_string_in_OnConfiguring() + { + var serviceProvider + = new ServiceCollection() + .AddDbContext() + .BuildServiceProvider(); + + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = serviceProvider.GetRequiredService(); + + context.Database.SetConnectionString(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + + Assert.True(context.Customers.Any()); + } + } + + [ConditionalFact] + public void Can_specify_no_connection_string_in_OnConfiguring_with_default_service_provider() + { + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = new NoneInOnConfiguringContext(); + + context.Database.SetConnectionString(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + + Assert.True(context.Customers.Any()); + } + } + + [ConditionalFact] + public void Throws_if_context_used_with_no_connection_or_connection_string() + { + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = new NoneInOnConfiguringContext(); + + Assert.Equal( + RelationalStrings.NoConnectionOrConnectionString, + Assert.Throws( + () => context.Customers.Any()).Message); + } + } + + private class NoneInOnConfiguringContext : NorthwindContextBase + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .EnableServiceProviderCaching(false) + .UseSqlServer(b => b.ApplyConfiguration()); + } + [ConditionalFact] public void Can_specify_connection_string_in_OnConfiguring() { @@ -51,6 +104,39 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) .UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString, b => b.ApplyConfiguration()); } + [ConditionalFact] + public void Can_specify_no_connection_in_OnConfiguring() + { + var serviceProvider + = new ServiceCollection() + .AddScoped(p => new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString)) + .AddDbContext().BuildServiceProvider(); + + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = serviceProvider.GetRequiredService(); + + using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + context.Database.SetDbConnection(connection); + + Assert.True(context.Customers.Any()); + } + } + + [ConditionalFact] + public void Can_specify_no_connection_in_OnConfiguring_with_default_service_provider() + { + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = new NoneInOnConfiguringContext(); + + using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + context.Database.SetDbConnection(connection); + + Assert.True(context.Customers.Any()); + } + } + [ConditionalFact] public void Can_specify_connection_in_OnConfiguring() { @@ -71,9 +157,61 @@ public void Can_specify_connection_in_OnConfiguring_with_default_service_provide { using (SqlServerTestStore.GetNorthwindStore()) { - using var context = new ConnectionInOnConfiguringContext( - new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString)); + using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + using var context = new ConnectionInOnConfiguringContext(connection); + + Assert.True(context.Customers.Any()); + } + } + + [ConditionalFact] + public void Can_specify_then_change_connection() + { + var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + + var serviceProvider + = new ServiceCollection() + .AddScoped(p => connection) + .AddDbContext().BuildServiceProvider(); + + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = serviceProvider.GetRequiredService(); + + Assert.Same(connection, context.Database.GetDbConnection()); + Assert.True(context.Customers.Any()); + + using var newConnection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + context.Database.SetDbConnection(newConnection); + + Assert.Same(newConnection, context.Database.GetDbConnection()); + Assert.True(context.Customers.Any()); + } + } + + [ConditionalFact] + public void Cannot_change_connection_when_open() + { + var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + + var serviceProvider + = new ServiceCollection() + .AddScoped(p => connection) + .AddDbContext().BuildServiceProvider(); + + using (SqlServerTestStore.GetNorthwindStore()) + { + using var context = serviceProvider.GetRequiredService(); + + context.Database.OpenConnection(); + Assert.Same(connection, context.Database.GetDbConnection()); Assert.True(context.Customers.Any()); + + using var newConnection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + + Assert.Equal( + RelationalStrings.CannotChangeWhenOpen, + Assert.Throws(() => context.Database.SetDbConnection(newConnection)).Message); } } @@ -158,9 +296,12 @@ public void Can_depend_on_DbContextOptions_with_default_service_provider() { using (SqlServerTestStore.GetNorthwindStore()) { + using var connection = new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString); + using var context = new OptionsContext( new DbContextOptions(), - new SqlConnection(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString)); + connection); + Assert.True(context.Customers.Any()); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs index bd4eb8ef960..9ff9845f248 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; diff --git a/test/EFCore.SqlServer.FunctionalTests/TwoDatabasesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TwoDatabasesSqlServerTest.cs new file mode 100644 index 00000000000..fa201722dba --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/TwoDatabasesSqlServerTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class TwoDatabasesSqlServerTest : TwoDatabasesTestBase, IClassFixture + { + public TwoDatabasesSqlServerTest(SqlServerFixture fixture) + : base(fixture) + { + } + + protected new SqlServerFixture Fixture => (SqlServerFixture)base.Fixture; + + protected override DbContextOptionsBuilder CreateTestOptions( + DbContextOptionsBuilder optionsBuilder, bool withConnectionString = false) + => withConnectionString + ? optionsBuilder.UseSqlServer(DummyConnectionString) + : optionsBuilder.UseSqlServer(); + + protected override TwoDatabasesWithDataContext CreateBackingContext(string databaseName) + => new TwoDatabasesWithDataContext(Fixture.CreateOptions(SqlServerTestStore.Create(databaseName))); + + protected override string DummyConnectionString { get; } = "Database=DoesNotExist"; + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/TwoDatabasesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/TwoDatabasesSqliteTest.cs new file mode 100644 index 00000000000..0284164201c --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/TwoDatabasesSqliteTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class TwoDatabasesSqliteTest : TwoDatabasesTestBase, IClassFixture + { + public TwoDatabasesSqliteTest(TwoDatabasesFixture fixture) + : base(fixture) + { + } + + protected new TwoDatabasesFixture Fixture => (TwoDatabasesFixture)base.Fixture; + + protected override DbContextOptionsBuilder CreateTestOptions( + DbContextOptionsBuilder optionsBuilder, bool withConnectionString = false) + => withConnectionString + ? optionsBuilder.UseSqlite(DummyConnectionString) + : optionsBuilder.UseSqlite(); + + protected override TwoDatabasesWithDataContext CreateBackingContext(string databaseName) + => new TwoDatabasesWithDataContext(Fixture.CreateOptions(SqliteTestStore.Create(databaseName))); + + protected override string DummyConnectionString { get; } = "DataSource=DummyDatabase"; + + public class TwoDatabasesFixture : ServiceProviderFixtureBase + { + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + } + } +} diff --git a/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs b/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs index 655990613bc..9a50e430b83 100644 --- a/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs +++ b/test/EFCore.Sqlite.Tests/SqliteDbContextOptionsBuilderExtensionsTest.cs @@ -70,6 +70,19 @@ public void Can_add_extension_with_connection() Assert.Null(extension.ConnectionString); } + [ConditionalFact] + public void Can_add_extension_with_no_connection() + { + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseSqlite(); + + var extension = optionsBuilder.Options.Extensions.OfType().Single(); + + Assert.Null(extension.Connection); + Assert.Null(extension.ConnectionString); + } + [ConditionalFact] public void Can_add_extension_with_connection_using_generic_options() {