Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionScope.Dispose causes a A root ambient transaction was completed before the nested transaction. The nested transactions should be completed first error #33726

Open
SpaceOgre opened this issue May 15, 2024 · 0 comments

Comments

@SpaceOgre
Copy link

File a bug

We are seeing an intermittent error in EF core when running our integration tests. We have not been able to reproduce it consistently and only see it once in a while in Azure DevOps when doing our PR builds that runs all the tests.

Using xUnit and a WebApplicationFactory we are calling our API that is using EF Core to access a database in a docker container.
Since we don't want tests to change data for other tests we have an attribute that creates a TransactionScope and Disposes it when the test is done (see code for it bellow). For this to work we set this Server.PreserveExecutionContext = true; in the WebApplicationFactory.

Not sure if there is a problem in how HandleTransactionCompleted does things when it is a dispose? or if this is a weird bug caused by a combination of things.

Include your code

The last test that failed:

[Theory, AutoRollback]
[MemberData(nameof(TestUsersForSchoolAccess.GetTestUsersWithSchoolReadAccess), MemberType = typeof(TestUsersForSchoolAccess))]
internal async Task Get_Returns_Ok_When_User_Have_SchoolReadAccess(TestUser testUser)
{
    // Arrange
    var request = new IdRequest(TestConstants.TestSchoolAleGymnasium.Id);

    // Act
    var (response, _) = await _apiWebFactory
        .CreateAuthenticatedClientFakeBearer(userIdHeader: testUser.Id)
        .GETAsync<GetSchoolEndpoint, IdRequest, SchoolBasicInfoResponse>(request);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

Autorollback attribute:

/// <summary>
/// Apply this attribute to your test method to automatically create a <see cref="TransactionScope"/>
/// that is rolled back when the test is finished.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
public sealed class AutoRollbackAttribute : BeforeAfterTestAttribute
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
{
    private TransactionScope _scope;

    /// <summary>
    /// Gets or sets whether transaction flow across thread continuations is enabled for TransactionScope.
    /// By default transaction flow across thread continuations is enabled.
    /// </summary>
    public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; } = TransactionScopeAsyncFlowOption.Enabled;

    /// <summary>
    /// Gets or sets the isolation level of the transaction.
    /// Default value is <see cref="IsolationLevel"/>.Serializable since we only care for correctness and not performance.
    /// </summary>
    public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.Serializable;

    /// <summary>
    /// Gets or sets the scope option for the transaction.
    /// Default value is <see cref="TransactionScopeOption"/>.Required.
    /// </summary>
    public TransactionScopeOption ScopeOption { get; set; } = TransactionScopeOption.RequiresNew;

    /// <summary>
    /// Gets or sets the timeout of the transaction, in milliseconds.
    /// By default, the transaction will not timeout.
    /// </summary>
    public long TimeoutInMS { get; set; } = -1;

    /// <summary>
    /// Rolls back the transaction.
    /// </summary>
    public override void After(MethodInfo methodUnderTest)
    {
        _scope.Dispose();
    }

    /// <summary>
    /// Creates the transaction.
    /// </summary>
    public override void Before(MethodInfo methodUnderTest)
    {
        var options = new TransactionOptions { IsolationLevel = IsolationLevel };
        if (TimeoutInMS > 0)
            options.Timeout = TimeSpan.FromMilliseconds(TimeoutInMS);

        _scope = new TransactionScope(ScopeOption, options, AsyncFlowOption);
    }
}

Include stack traces

System.InvalidOperationException : A root ambient transaction was completed before the nested transaction. The nested transactions should be completed first.
  at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.HandleTransactionCompleted(Object sender, TransactionEventArgs e)
  at System.Transactions.TransactionStateAborted.EnterState(InternalTransaction tx)
  at System.Transactions.Transaction.Rollback()
  at System.Transactions.TransactionScope.InternalDispose()
  at System.Transactions.TransactionScope.Dispose()
  at GR.Elevinformation.API.IntegrationTests.AutoRollbackAttribute.After(MethodInfo methodUnderTest) in /home/vsts/work/1/s/tests/GR.Elevinformation.API.IntegrationTests/AutoRollbackAttribute.cs:line 47

Include provider and version information

EF Core version: 8.0.5
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Ubuntu 22.04.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants