Skip to content

Commit

Permalink
Allow DbConnection or connection string to be changed on existing DbC…
Browse files Browse the repository at this point in the history
…ontext

Fixes #6525 (Allow connections string to be set/changed)
Fixes #8494 (Allow connection to be set/changed)
Fixes #8427 (Parameterless overloads of UseSqlServer, UseSqlite)
  • Loading branch information
ajcvickers committed Dec 23, 2019
1 parent b3a89d0 commit bde2b14
Show file tree
Hide file tree
Showing 24 changed files with 761 additions and 52 deletions.
Expand Up @@ -385,6 +385,43 @@ public static IEnumerable<string> GetPendingMigrations([NotNull] this DatabaseFa
public static DbConnection GetDbConnection([NotNull] this DatabaseFacade databaseFacade)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection;

/// <summary>
/// <para>
/// Sets the underlying ADO.NET <see cref="DbConnection" /> for this <see cref="DbContext" />.
/// </para>
/// <para>
/// The connection can only be set when the existing connection, if any, is not open.
/// </para>
/// <para>
/// Note that the given connection must be disposed by application code since it was not created by Entity Framework.
/// </para>
/// </summary>
/// <param name="databaseFacade"> The <see cref="DatabaseFacade" /> for the context. </param>
/// <param name="connection"> The connection. </param>
public static void SetDbConnection([NotNull] this DatabaseFacade databaseFacade, [CanBeNull] DbConnection connection)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection = connection;

/// <summary>
/// Gets the underlying connection string configured for this <see cref="DbContext" />.
/// </summary>
/// <param name="databaseFacade"> The <see cref="DatabaseFacade" /> for the context. </param>
/// <returns> The connection string. </returns>
public static string GetConnectionString([NotNull] this DatabaseFacade databaseFacade)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.ConnectionString;

/// <summary>
/// <para>
/// Sets the underlying connection string configured for this <see cref="DbContext" />.
/// </para>
/// <para>
/// It may not be possible to change the connection string if existing connection, if any, is open.
/// </para>
/// </summary>
/// <param name="databaseFacade"> The <see cref="DatabaseFacade" /> for the context. </param>
/// <param name="connectionString"> The connection string. </param>
public static void SetConnectionString([NotNull] this DatabaseFacade databaseFacade, [CanBeNull] string connectionString)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.ConnectionString = connectionString;

/// <summary>
/// Opens the underlying <see cref="DbConnection" />.
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Expand Up @@ -123,6 +123,9 @@
<data name="NoDbCommand" xml:space="preserve">
<value>Cannot create a 'DbCommand' for a non-relational query.</value>
</data>
<data name="CannotChangeWhenOpen" xml:space="preserve">
<value>The 'DbConnection' is currently in use. The connection can only be changed when the existing connection is not being used.</value>
</data>
<data name="UpdateConcurrencyException" xml:space="preserve">
<value>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.</value>
</data>
Expand Down
33 changes: 29 additions & 4 deletions src/EFCore.Relational/Storage/IRelationalConnection.cs
Expand Up @@ -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
Expand All @@ -27,14 +28,37 @@ namespace Microsoft.EntityFrameworkCore.Storage
public interface IRelationalConnection : IRelationalTransactionManager, IDisposable, IAsyncDisposable
{
/// <summary>
/// Gets the connection string for the database.
/// Gets or sets the connection string for the database.
/// </summary>
string ConnectionString { get; }
string ConnectionString
{
get => throw new NotImplementedException();
[param: CanBeNull] set => throw new NotImplementedException();
}

/// <summary>
/// Gets the underlying <see cref="System.Data.Common.DbConnection" /> used to connect to the database.
/// Returns the configured connection string only if it has been set or a valid <see cref="DbConnection" /> exists.
/// </summary>
DbConnection DbConnection { get; }
/// <returns> The connection string. </returns>
/// <exception cref="InvalidOperationException"> when connection string cannot be obtained. </exception>
string GetCheckedConnectionString() => ConnectionString;

/// <summary>
/// <para>
/// Gets or sets the underlying <see cref="System.Data.Common.DbConnection" /> used to connect to the database.
/// </para>
/// <para>
/// The connection can only be changed when the existing connection, if any, is not open.
/// </para>
/// <para>
/// Note that the connection must be disposed by application code since it was not created by Entity Framework.
/// </para>
/// </summary>
DbConnection DbConnection
{
get => throw new NotImplementedException();
[param: CanBeNull] set => throw new NotImplementedException();
}

/// <summary>
/// The <see cref="DbContext" /> currently in use, or null if not known.
Expand Down Expand Up @@ -97,6 +121,7 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa
/// <value>
/// The semaphore.
/// </value>
[Obsolete("EF Core no longer uses this semaphore. It will be removed in an upcoming release.")]
SemaphoreSlim Semaphore { get; }
}
}
100 changes: 83 additions & 17 deletions src/EFCore.Relational/Storage/RelationalConnection.cs
Expand Up @@ -34,8 +34,8 @@ namespace Microsoft.EntityFrameworkCore.Storage
/// </summary>
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;
Expand All @@ -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;
}
}

Expand All @@ -101,15 +100,80 @@ protected RelationalConnection([NotNull] RelationalConnectionDependencies depend
protected abstract DbConnection CreateDbConnection();

/// <summary>
/// Gets the connection string for the database.
/// Gets or sets the connection string for the database.
/// </summary>
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;
}
}

/// <summary>
/// Gets the underlying <see cref="System.Data.Common.DbConnection" /> used to connect to the database.
/// Returns the configured connection string only if it has been set or a valid <see cref="DbConnection" /> exists.
/// </summary>
/// <returns> The connection string. </returns>
/// <exception cref="InvalidOperationException"> when connection string cannot be obtained. </exception>
public virtual string GetCheckedConnectionString()
{
var connectionString = ConnectionString;
if (connectionString == null)
{
throw new InvalidOperationException(RelationalStrings.NoConnectionOrConnectionString);
}

return connectionString;
}

/// <summary>
/// <para>
/// Gets or sets the underlying <see cref="System.Data.Common.DbConnection" /> used to connect to the database.
/// </para>
/// <para>
/// The connection can only be changed when the existing connection, if any, is not open.
/// </para>
/// <para>
/// Note that a connection set must be disposed by application code since it was not created by Entity Framework.
/// </para>
/// </summary>
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;
}
}
}

/// <summary>
/// Gets the current transaction.
Expand Down Expand Up @@ -737,6 +801,7 @@ private bool ShouldClose()
/// <value>
/// The semaphore used to serialize access to this connection.
/// </value>
[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;
Expand All @@ -755,6 +820,7 @@ public virtual void Dispose()
DbConnection.Dispose();
_connection = null;
_openedCount = 0;
_openedInternally = false;
}
}

Expand Down
Expand Up @@ -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;
Expand Down
Expand Up @@ -17,6 +17,35 @@ namespace Microsoft.EntityFrameworkCore
/// </summary>
public static class SqlServerDbContextOptionsExtensions
{
/// <summary>
/// <para>
/// Configures the context to connect to a Microsoft SQL Server database, but without initially setting any
/// <see cref="DbConnection" /> or connection string.
/// </para>
/// <para>
/// The connection or connection string must be set before the <see cref="DbContext" /> is used to connect
/// to a database. Set a connection using <see cref="RelationalDatabaseFacadeExtensions.SetDbConnection" />.
/// Set a connection string using <see cref="RelationalDatabaseFacadeExtensions.SetConnectionString" />.
/// </para>
/// </summary>
/// <param name="optionsBuilder"> The builder being used to configure the context. </param>
/// <param name="sqlServerOptionsAction">An optional action to allow additional SQL Server specific configuration.</param>
/// <returns> The options builder so that further configuration can be chained. </returns>
public static DbContextOptionsBuilder UseSqlServer(
[NotNull] this DbContextOptionsBuilder optionsBuilder,
[CanBeNull] Action<SqlServerDbContextOptionsBuilder> sqlServerOptionsAction = null)
{
Check.NotNull(optionsBuilder, nameof(optionsBuilder));

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(GetOrCreateExtension(optionsBuilder));

ConfigureWarnings(optionsBuilder);

sqlServerOptionsAction?.Invoke(new SqlServerDbContextOptionsBuilder(optionsBuilder));

return optionsBuilder;
}

/// <summary>
/// Configures the context to connect to a Microsoft SQL Server database.
/// </summary>
Expand Down Expand Up @@ -72,6 +101,27 @@ public static class SqlServerDbContextOptionsExtensions
return optionsBuilder;
}

/// <summary>
/// <para>
/// Configures the context to connect to a Microsoft SQL Server database, but without initially setting any
/// <see cref="DbConnection" /> or connection string.
/// </para>
/// <para>
/// The connection or connection string must be set before the <see cref="DbContext" /> is used to connect
/// to a database. Set a connection using <see cref="RelationalDatabaseFacadeExtensions.SetDbConnection" />.
/// Set a connection string using <see cref="RelationalDatabaseFacadeExtensions.SetConnectionString" />.
/// </para>
/// </summary>
/// <param name="optionsBuilder"> The builder being used to configure the context. </param>
/// <param name="sqlServerOptionsAction">An optional action to allow additional SQL Server specific configuration.</param>
/// <returns> The options builder so that further configuration can be chained. </returns>
public static DbContextOptionsBuilder<TContext> UseSqlServer<TContext>(
[NotNull] this DbContextOptionsBuilder<TContext> optionsBuilder,
[CanBeNull] Action<SqlServerDbContextOptionsBuilder> sqlServerOptionsAction = null)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseSqlServer(
(DbContextOptionsBuilder)optionsBuilder, sqlServerOptionsAction);

/// <summary>
/// Configures the context to connect to a Microsoft SQL Server database.
/// </summary>
Expand Down
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.SqlServer/SqlServerRetryingExecutionStrategy.cs
Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions src/EFCore.SqlServer/Storage/Internal/SqlServerConnection.cs
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
/// </summary>
protected override DbConnection CreateDbConnection() => new SqlConnection(ConnectionString);
protected override DbConnection CreateDbConnection() => new SqlConnection(GetCheckedConnectionString());

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -55,7 +55,7 @@ public SqlServerConnection([NotNull] RelationalConnectionDependencies dependenci
/// </summary>
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()
Expand Down

0 comments on commit bde2b14

Please sign in to comment.