Skip to content

Commit

Permalink
Allow transfer of ownership of DbConnection from application to DbCon…
Browse files Browse the repository at this point in the history
…text

Fixes #24199
  • Loading branch information
ajcvickers committed Jan 5, 2023
1 parent 35a9fe3 commit ea79b63
Show file tree
Hide file tree
Showing 13 changed files with 420 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </para>
/// <para>
/// Note that the given connection must be disposed by application code since it was not created by Entity Framework.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-connections">Connections and connection strings</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="connection">The connection.</param>
public static void SetDbConnection(this DatabaseFacade databaseFacade, DbConnection? connection)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.DbConnection = connection;
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal. The default value is <see langword="false"/>.
/// </param>
public static void SetDbConnection(this DatabaseFacade databaseFacade, DbConnection? connection, bool contextOwnsConnection = false)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.SetDbConnection(connection, contextOwnsConnection);

/// <summary>
/// Gets the underlying connection string configured for this <see cref="DbContext" />.
Expand Down
19 changes: 19 additions & 0 deletions src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -101,17 +103,34 @@ public virtual RelationalOptionsExtension WithConnectionString(string? connectio
public virtual DbConnection? Connection
=> _connection;

/// <summary>
/// <see langword="true"/> if the <see cref="Connection"/> is owned by the context and should be disposed appropriately.
/// </summary>
public virtual bool ConnectionOwned
=> _connectionOwned;

/// <summary>
/// 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 <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="connection">The option to change.</param>
/// <returns>A new instance with the option changed.</returns>
public virtual RelationalOptionsExtension WithConnection(DbConnection? connection)
=> WithConnection(connection, owned: false);

/// <summary>
/// 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 <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="connection">The option to change.</param>
/// <param name="owned">If <see langword="true"/>, 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.</param>
/// <returns>A new instance with the option changed.</returns>
public virtual RelationalOptionsExtension WithConnection(DbConnection? connection, bool owned)
{
var clone = Clone();

clone._connection = connection;
clone._connectionOwned = owned;

return clone;
}
Expand Down
16 changes: 16 additions & 0 deletions src/EFCore.Relational/Storage/IRelationalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa
[AllowNull]
DbConnection DbConnection { get; set; }

/// <summary>
/// Sets the underlying <see cref="System.Data.Common.DbConnection" /> used to connect to the database.
/// </summary>
/// <param name="value">The connection object.</param>
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal.
/// </param>
/// <remarks>
/// <para>
/// The connection can only be changed when the existing connection, if any, is not open.
/// </para>
/// </remarks>
void SetDbConnection(DbConnection? value, bool contextOwnsConnection);

/// <summary>
/// The <see cref="DbContext" /> currently in use, or <see langword="null" /> if not known.
/// </summary>
Expand Down
28 changes: 17 additions & 11 deletions src/EFCore.Relational/Storage/RelationalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected RelationalConnection(RelationalConnectionDependencies dependencies)
if (relationalOptions.Connection != null)
{
_connection = relationalOptions.Connection;
_connectionOwned = false;
_connectionOwned = relationalOptions.ConnectionOwned;

if (_connectionString != null)
{
Expand Down Expand Up @@ -172,19 +172,25 @@ public virtual DbConnection DbConnection
}
set
{
if (!ReferenceEquals(_connection, value))
SetDbConnection(value, contextOwnsConnection: false);
}
}

/// <inheritdoc />
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ public static DbContextOptionsBuilder UseSqlServer(
return optionsBuilder;
}

// Note: Decision made to use DbConnection not SqlConnection: Issue #772
/// <summary>
/// Configures the context to connect to a Microsoft SQL Server database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </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(
this DbContextOptionsBuilder optionsBuilder,
DbConnection connection,
Action<SqlServerDbContextOptionsBuilder>? sqlServerOptionsAction = null)
=> UseSqlServer(optionsBuilder, connection, false, sqlServerOptionsAction);

// Note: Decision made to use DbConnection not SqlConnection: Issue #772
/// <summary>
/// Configures the context to connect to a Microsoft SQL Server database.
Expand All @@ -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.
/// </param>
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal.
/// </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(
this DbContextOptionsBuilder optionsBuilder,
DbConnection connection,
bool contextOwnsConnection,
Action<SqlServerDbContextOptionsBuilder>? 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);
Expand Down Expand Up @@ -170,7 +200,8 @@ public static DbContextOptionsBuilder<TContext> UseSqlServer<TContext>(
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </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>
Expand All @@ -182,6 +213,38 @@ public static DbContextOptionsBuilder<TContext> UseSqlServer<TContext>(
=> (DbContextOptionsBuilder<TContext>)UseSqlServer(
(DbContextOptionsBuilder)optionsBuilder, connection, sqlServerOptionsAction);

// Note: Decision made to use DbConnection not SqlConnection: Issue #772
/// <summary>
/// Configures the context to connect to a Microsoft SQL Server database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <typeparam name="TContext">The type of context to be configured.</typeparam>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </param>
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal.
/// </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>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
DbConnection connection,
bool contextOwnsConnection,
Action<SqlServerDbContextOptionsBuilder>? sqlServerOptionsAction = null)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseSqlServer(
(DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, sqlServerOptionsAction);

private static SqlServerOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.Options.FindExtension<SqlServerOptionsExtension>()
?? new SqlServerOptionsExtension();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ public static DbContextOptionsBuilder UseSqlite(
return optionsBuilder;
}

/// <summary>
/// Configures the context to connect to a SQLite database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </param>
/// <param name="sqliteOptionsAction">An optional action to allow additional SQLite specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
public static DbContextOptionsBuilder UseSqlite(
this DbContextOptionsBuilder optionsBuilder,
DbConnection connection,
Action<SqliteDbContextOptionsBuilder>? sqliteOptionsAction = null)
=> UseSqlite(optionsBuilder, connection, false, sqliteOptionsAction);

/// <summary>
/// Configures the context to connect to a SQLite database.
/// </summary>
Expand All @@ -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.
/// </param>
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal.
/// </param>
/// <param name="sqliteOptionsAction">An optional action to allow additional SQLite specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
public static DbContextOptionsBuilder UseSqlite(
this DbContextOptionsBuilder optionsBuilder,
DbConnection connection,
bool contextOwnsConnection,
Action<SqliteDbContextOptionsBuilder>? 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);
Expand Down Expand Up @@ -161,7 +189,8 @@ public static DbContextOptionsBuilder<TContext> UseSqlite<TContext>(
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </param>
/// <param name="sqliteOptionsAction">An optional action to allow additional SQLite specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
Expand All @@ -173,6 +202,36 @@ public static DbContextOptionsBuilder<TContext> UseSqlite<TContext>(
=> (DbContextOptionsBuilder<TContext>)UseSqlite(
(DbContextOptionsBuilder)optionsBuilder, connection, sqliteOptionsAction);

/// <summary>
/// Configures the context to connect to a SQLite database.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlite">Accessing SQLite databases with EF Core</see> for more information and examples.
/// </remarks>
/// <typeparam name="TContext">The type of context to be configured.</typeparam>
/// <param name="optionsBuilder">The builder being used to configure the context.</param>
/// <param name="connection">
/// An existing <see cref="DbConnection" /> 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.
/// </param>
/// <param name="contextOwnsConnection">
/// If <see langword="true" />, 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 <see langword="false" />, then the caller still
/// owns the connection and is responsible for its disposal.
/// </param>
/// <param name="sqliteOptionsAction">An optional action to allow additional SQLite specific configuration.</param>
/// <returns>The options builder so that further configuration can be chained.</returns>
public static DbContextOptionsBuilder<TContext> UseSqlite<TContext>(
this DbContextOptionsBuilder<TContext> optionsBuilder,
DbConnection connection,
bool contextOwnsConnection,
Action<SqliteDbContextOptionsBuilder>? sqliteOptionsAction = null)
where TContext : DbContext
=> (DbContextOptionsBuilder<TContext>)UseSqlite(
(DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, sqliteOptionsAction);

private static SqliteOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder options)
=> options.Options.FindExtension<SqliteOptionsExtension>()
?? new SqliteOptionsExtension();
Expand Down
Loading

0 comments on commit ea79b63

Please sign in to comment.