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 22, 2019
1 parent 2aefa91 commit 0e31df4
Show file tree
Hide file tree
Showing 24 changed files with 761 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,43 @@ public static async Task<int> ExecuteSqlRawAsync(
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 DbContextOptionsBuilder UseSqlServer(
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 0e31df4

Please sign in to comment.