Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -10771,6 +10771,12 @@
{
"Member": "static System.Linq.IQueryable<TResult> SqlQueryRaw<TResult>(this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade, string sql, params object?[] parameters);"
},
{
"Member": "static System.IDisposable UseCommandTimeout(this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade, int? timeout);"
},
{
"Member": "static System.IDisposable UseCommandTimeout(this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade, System.TimeSpan timeout);"
},
{
"Member": "static Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? UseTransaction(this Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade databaseFacade, System.Data.Common.DbTransaction? transaction);"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,63 @@ public static void SetCommandTimeout(this DatabaseFacade databaseFacade, TimeSpa
public static int? GetCommandTimeout(this DatabaseFacade databaseFacade)
=> GetFacadeDependencies(databaseFacade).RelationalConnection.CommandTimeout;

/// <summary>
/// Sets the timeout to use for commands executed with this <see cref="DbContext" /> within the returned scope.
/// The previous timeout is restored when the returned object is disposed.
/// </summary>
/// <remarks>
/// <para>
/// Note that the command timeout is distinct from the connection timeout. Connection timeouts are usually
/// configured in the connection string. More recently, some ADO.NET data providers are adding the capability
/// to also set a command timeout in the connection string. A value set with this API for the command timeout
/// will override any value set in the connection string.
/// </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="timeout">The timeout to use, in seconds.</param>
/// <returns>An <see cref="IDisposable" /> that resets the command timeout to its previous value when disposed.</returns>
Comment on lines +1011 to +1012
public static IDisposable UseCommandTimeout(this DatabaseFacade databaseFacade, int? timeout)
=> new CommandTimeoutScope(databaseFacade, timeout);

/// <summary>
/// Sets the timeout to use for commands executed with this <see cref="DbContext" /> within the returned scope.
/// The previous timeout is restored when the returned object is disposed.
/// </summary>
/// <remarks>
/// <para>
/// This is a sugar method allowing a <see cref="TimeSpan" /> to be used to set the value. It delegates to
/// <see cref="UseCommandTimeout(DatabaseFacade,int?)" />.
/// </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="timeout">The timeout to use.</param>
/// <returns>An <see cref="IDisposable" /> that resets the command timeout to its previous value when disposed.</returns>
public static IDisposable UseCommandTimeout(this DatabaseFacade databaseFacade, TimeSpan timeout)
{
if (timeout == Timeout.InfiniteTimeSpan)
{
return databaseFacade.UseCommandTimeout(0);
}

if (timeout < TimeSpan.Zero)
{
throw new ArgumentException(RelationalStrings.TimeoutTooSmall(timeout.TotalSeconds));
}

if (timeout.TotalSeconds > int.MaxValue)
{
throw new ArgumentException(RelationalStrings.TimeoutTooBig(timeout.TotalSeconds));
}

return databaseFacade.UseCommandTimeout(Convert.ToInt32(timeout.TotalSeconds));
}

/// <summary>
/// Generates a script to create all tables for the current model.
/// </summary>
Expand Down Expand Up @@ -1052,4 +1109,27 @@ private static TService GetRelationalService<TService>(this IInfrastructure<ISer

private static IDbContextTransactionManager GetTransactionManager(this DatabaseFacade databaseFacade)
=> ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Dependencies.TransactionManager;

private sealed class CommandTimeoutScope : IDisposable
{
private readonly DatabaseFacade _databaseFacade;
private readonly int? _previousTimeout;
private bool _disposed;

public CommandTimeoutScope(DatabaseFacade databaseFacade, int? timeout)
{
_databaseFacade = databaseFacade;
_previousTimeout = databaseFacade.GetCommandTimeout();
databaseFacade.SetCommandTimeout(timeout);
}

public void Dispose()
{
if (!_disposed)
{
_databaseFacade.SetCommandTimeout(_previousTimeout);
_disposed = true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public void Relational_specific_methods_throws_when_non_relational_provider_is_i
Assert.Equal(
RelationalStrings.RelationalNotInUse,
Assert.Throws<InvalidOperationException>(context.Database.GetDbConnection).Message);
Assert.Equal(
RelationalStrings.RelationalNotInUse,
Assert.Throws<InvalidOperationException>(() => context.Database.UseCommandTimeout(10)).Message);
}

[Theory, InlineData(true), InlineData(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,83 @@ public void Setting_CommandTimeout_to_negative_value_throws()
Assert.Throws<ArgumentException>(() => context.Database.SetCommandTimeout(TimeSpan.FromSeconds(uint.MaxValue)));
}

[Fact]
public void UseCommandTimeout_scopes_timeout_and_restores_previous_value_on_dispose()
{
using var context = new TimeoutContext();
Assert.Null(context.Database.GetCommandTimeout());

using (context.Database.UseCommandTimeout(77))
{
Assert.Equal(77, context.Database.GetCommandTimeout());

using (context.Database.UseCommandTimeout(TimeSpan.FromSeconds(88)))
{
Assert.Equal(88, context.Database.GetCommandTimeout());

using (context.Database.UseCommandTimeout(null))
{
Assert.Null(context.Database.GetCommandTimeout());
}

Assert.Equal(88, context.Database.GetCommandTimeout());
}

Assert.Equal(77, context.Database.GetCommandTimeout());
}

Assert.Null(context.Database.GetCommandTimeout());
}

[Fact]
public void UseCommandTimeout_restores_previous_value_when_exception_thrown()
{
using var context = new TimeoutContext();
context.Database.SetCommandTimeout(50);

Assert.Throws<NotSupportedException>(
(Action)(() =>
{
using (context.Database.UseCommandTimeout(100))
{
Assert.Equal(100, context.Database.GetCommandTimeout());
throw new NotSupportedException();
}
}));

Assert.Equal(50, context.Database.GetCommandTimeout());
}

[Fact]
public void UseCommandTimeout_with_infinite_timespan_sets_to_zero()
{
using var context = new TimeoutContext();
context.Database.SetCommandTimeout(50);

using (context.Database.UseCommandTimeout(Timeout.InfiniteTimeSpan))
{
Assert.Equal(0, context.Database.GetCommandTimeout());
}

Assert.Equal(50, context.Database.GetCommandTimeout());
}

[Fact]
public void UseCommandTimeout_to_negative_value_throws_and_does_not_change_timeout()
{
using var context = new TimeoutContext();
context.Database.SetCommandTimeout(50);

Assert.Throws<ArgumentException>(() => context.Database.UseCommandTimeout(-3));
Assert.Equal(50, context.Database.GetCommandTimeout());

Assert.Throws<ArgumentException>(() => context.Database.UseCommandTimeout(TimeSpan.FromSeconds(-3)));
Assert.Equal(50, context.Database.GetCommandTimeout());

Assert.Throws<ArgumentException>(() => context.Database.UseCommandTimeout(TimeSpan.FromSeconds(uint.MaxValue)));
Assert.Equal(50, context.Database.GetCommandTimeout());
}

public class TimeoutContext : DbContext
{
public TimeoutContext()
Expand Down
Loading