The SQL Standard defines the concept of savepoints, which allow one to partially roll back a transaction; multiple savepoints may be defined, but only within a transaction. These seem to be well-supported in a uniform way across databases - although the SQL syntax is a bit quirky across databases. SqlClient and Npgsql already expose methods for savepoints on their DbTransaction implementations, but there's probably value in exposing these on DbTransaction.
```c#
class DbTransaction
{
public virtual Task SaveAsync(string savepointName, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public virtual Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public virtual Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public virtual void Save(string savepointName) => throw new NotSupportedException();
public virtual void Rollback(string savepointName) => throw new NotSupportedException();
public virtual void Release(string savepointName) {}
public virtual bool AreSavepointsSupported => false; // Alternate naming: SupportsSavepoints
}
```
AreSavepointsSupported should throw InvalidOperationException if accessed with closed connection. We could introduce a non-virtual user-facing property to check for this, calling a protected virtual property implemented by the provider, but we can't know if a transaction has completed (or has been disposed). So it's probably better to leave this to the provider, documenting the correct behavior (even when the specific provider always supports savepoints).| Date | Modification |
| ----- | ------------ |
| 18/3 | API proposal
| 24/3 | Renamed parameter name from savePointName to savepointName
| 26/3 | Made AreSavepointsSupported virtual
| 21/7 | Added alternate naming Supports Savepoint, describe alternative API (CreateSavepoint instead of Save)
/cc @David-Engel @cheenamalhotra
Added API proposal with notes
/cc @David-Engel @cheenamalhotra @bgrainger @bricelam @ajcvickers @stephentoub @terrajobst @mgravell
RELEASE ... has no user-visible behavior ... so it seems correct to have this be a no-op by default
There can be user-visible side-effects (e.g., raising an error if that savepoint name doesn't exist, making it an error in the future to roll back to that savepoint name after it's released).
My inclination is to make it throw NotSupportedException by default (since it won't be supported by older providers running on top of the new base class) and allow providers to override it as a noop if they choose.
Since DB terminology universally uses "savepoint" (not "save point"; see docs linked from OP), the parameter names should be savepointName. That's also consistent with AreSavepointsSupported.
Sorry for the delayed response @bgrainger, these are busy times.
There can be user-visible side-effects (e.g., raising an error if that savepoint name doesn't exist, making it an error in the future to roll back to that savepoint name after it's released).
Right - release can be visible for code depending on an error being thrown when the API is used incorrectly etc. I don't have very strong feelings here, but I don't think these negative cases typically inform API decisions in a significant way.
My inclination is to make it throw NotSupportedException by default [...]
To be concrete, all databases I know of which support savepoints support Release, except SQL Server where this would be a no-op; so we're mainly discussing whether SqlClient would inherit the no-op behavior or if it would have to override for that behavior. This would have very little actual consequences either way.
[...] (since it won't be supported by older providers running on top of the new base class) and allow providers to override it as a noop if they choose.
Well, anyone using the new savepoint API on older providers is very likely to call Save/Rollback before Release, at which point they'd get the NotSupportedException anyway.
Since DB terminology universally uses "savepoint" (not "save point"; see docs linked from OP), the parameter names should be savepointName. That's also consistent with AreSavepointsSupported.
Sounds good. The naming was taken from SqlClient's current naming, but the new DbTransaction methods can have different parameter names (though this may get slightly confusing for people using named parameters). FWIW Npgsql already uses a different parameter name here (name). Amended the proposal.
This would have very little actual consequences either way.
Agreed, and I'm happy with the current proposal.
Amended the proposal.
Thanks.
One final thing: shouldn't this be virtual?
public bool AreSavepointsSupported => false;
One final thing: shouldn't this be virtual?
Absolutely, thanks for catching it (amended).
I think this is ready for review - though am wondering if return a Task rather than ValueTask is still the right thing here (/cc @stephentoub). Am marking this as ready for review.
PR submitted: #34561. This is pretty much a simple copy-paste of the proposal above, but @ajcvickers @bgrainger always good to have another pair eyes to make sure things are good.
am wondering if return a Task rather than ValueTask is still the right thing here
After discussion with @stephentoub, leaving these methods to return a Task as the allocation isn't likely to matter for methods which are almost certain to do a database network roundtrip, and which don't return a value.
@stephentoub @terrajobst I'm really hoping we can get this in for preview4 as EF Core needs to build some functionality over it.
@FransBouma just realized I had forgotten to cc you on this, in case you're interested.
@roji Looks good to me :) Yeah 'Release' isn't really that interesting, as in general you want to go back to a given point in time in the transaction. It's a niche feature tho. I support it through my ORM API, and use reflection to obtain the Save method on the ADO.NET provider so having a generic interface for this is great, saves me the trouble for reflecting over it :)
I support it through my ORM API, and use reflection to obtain the Save method on the ADO.NET provider
Great, that's the perfect example for why this is needed in System.Data :)
@ajcvickers @roji let me know if you still want to do this for .NET 5 and I'll schedule a review.
@terrajobst yes, if possible I'd still like to get this in for 5...
SupportsSavepointsDbSavepoint but we felt it's overkill for a niche feature like thisSystem.SqlClient and Microsoft.SqlClient APIs should be updated to override these new base methods (instead of just hiding them) as well as SupportsSavepoints.```C#
namespace System.Data.Common
{
public class DbTransaction
{
public virtual Task SaveAsync(string savepointName, CancellationToken cancellationToken = default);
public virtual Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default);
public virtual Task ReleaseAsync(string savepointName, CancellationToken cancellationToken = default);
public virtual void Save(string savepointName);
public virtual void Rollback(string savepointName);
public virtual void Release(string savepointName);
public virtual bool SupportsSavepoints { get; }
}
}
```
PR: #34561
Most helpful comment
Added API proposal with notes
/cc @David-Engel @cheenamalhotra @bgrainger @bricelam @ajcvickers @stephentoub @terrajobst @mgravell