Base solution for your next web application
Open Closed

UnitOfWork and TransactionScopeOption Suppress #9182


User avatar
0
Siyeza created

Hi,

I have a long running operation that creates a unit of work using UnitOfWorkManager.Begin(TransactionScopeOption.Suppress) in order to prevent table/row locks.

Inside this unit of work I have operations that create their own unit of works, either via the UnitOfWork attribute or via the UnitOfWorkManager. I've noticed two things:

  1. The inner unit of works seem to inherit TransactionScopeOption.Suppress as well., Methods with the UnitOfWork attribute will inherit TransactionScopeOption.Suppress.
  2. An inner unit of work created with UnitOfWorkManager.Begin(TransactionScopeOption.Require) has to wait for outer unit of work, created with TransactionScopeOption.Suppres, to complete before it's changes are committed

The behaviour of 2) seem wrong to me.

An inner unit of work started with UnitOfWorkManager.Begin(TransactionScopeOption.Required) should commit changes immediately when it has completed, regardless if the outer unit of work was created with TransactionScopeOption.Suppress.

Thanks


10 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team

    hi @Siyeza

    • Your Abp package version.
    • Your base framework: .Net Framework or .Net Core.

    Can you share some code?

  • User Avatar
    0
    Siyeza created

    Hi,

    ABP version is 5.6 Base framework is .NET Core

    I have an ApplicationService with the following method:

    /// <summary>
            /// Submits the specified item to the workflow
            /// </summary>
            /// <param name="request"></param>
            /// <returns></returns>                      
            public async Task SubmitAsync(ItemActionRequestDto request)
            {
                Guard.ArgumentNotNull(request, nameof(request));      
    
                using (var unitOfWork = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
                {
                    // Map the request
                    var workflowRequest = ObjectMapper.Map<ItemActionRequest>(request);
                    workflowRequest.UserId = AbpSession.UserId.Value;
    
                    // Submit the item to the workflow
                    var workflow = await ItemWorkflowFactory.CreateAsync(workflowRequest);
                    await workflow.SubmitAsync(workflowRequest);
    
                    unitOfWork.Complete();
                }
            }
    

    The workflow.SubmitAsync call will eventually call the following DomainService method:

    /// <summary>
            /// Creates the workflow item for the specified Item
            /// </summary>
            /// <param name="item"></param>                
            public virtual void CreateWorkflowItem(TItem item)
            {
                Guard.ArgumentNotNull(item, nameof(item));
    
                using(var unitOfWork = UnitOfWorkManager.Begin(TransactionScopeOption.Required))
                {
                    // Check if workflow item already exists
                    var workflowItem = WorkflowItemRepository.FirstOrDefault(w => w.ItemRef == item.Id);
                    if (workflowItem != null)
                    {
                        throw new InvalidOperationException(string.Format(InternalMessages.Workflow_ItemAlreadyExists, item.Id));
                    }
    
                    // Create workflow item
                    workflowItem = new WorkflowItem
                    {
                        ItemRef = item.Id,
                        ItemType = item.ItemType,
                        ItemSubTypeCode = item.ItemSubTypeCode,
                        ItemSubTypeName = GetItemSubTypeName(item),
                        ItemAction = item.ItemAction,
                        TenantId = item.TenantId,
                        Trigger = WorkflowTrigger.None,
                        State = WorkflowState.NotStarted
                    };
    
                    // Get the route for the item
                    var routingManager = GetRoutingManager(item);
                    workflowItem.RouteItemTypeCode = routingManager.Route.ItemTypeCode;
    
                    // Save the item
                    item.WorkflowItem = workflowItem;
                    ItemRepository.Update(item);
    
    
                    unitOfWork.Complete();
                }
    
               
            }
    
    

    The changes made in this inner UnitOfWork will not reflect in the DB until the outer UnitOfWork (created with TransactionScopeOption.Suppress) has completed.

    Thanks

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Could you try calling CurrentUnitOfWork.SaveChangesAsync(); in your inner unitOfWork ?

  • User Avatar
    0
    Siyeza created

    Hi,

    I tried CurrentUnitOfWork.SaveChangesAsync() but it doens't solve the problem. It saves the changes in the context of the transaction (IE. genrate entity IDs etc) but those changes don't show in the DB until the outer unit of work has completed.

    This is bad because it means the outer unit of work is keeping alive the transaction created in the inner unit of work. That goes against how TransactionScope.Suppress is supposed to work. The outer unit or work (created with TransactionScope.Suppress) is behaving like it was created with TransactionScope.Required.

    For example, consider the code below (in a non-ABP scenario). As far as I know, changes made in innerScope will complete and reflect as soon as the innerScope has completed. It won't wait for outerScope to complete first.

    using(var outerScope = new TransactionScope(TransactionScopeOption.Suppress)){
    
        using(var innerScope = new TransactionScope(TransactionScopeOption.Required)){
                // Make changes
                ...
                
                innerScope.Complete();
        }
    
        outerScope.Complete();
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @Siyeza,

    I tried CurrentUnitOfWork.SaveChangesAsync() but it doens't solve the problem. It saves the changes in the context of the transaction (IE. genrate entity IDs etc) but those changes don't show in the DB until the outer unit of work has completed.

    Yes, AspNEt Boilerplate's design is like that.

    For example, consider the code below (in a non-ABP scenario). As far as I know, changes made in innerScope will complete and reflect as soon as the innerScope has completed. It won't wait for outerScope to complete first.

    Could you test this ? I'm not sure about it, I will also test it.

    Thanks,

  • User Avatar
    0
    Siyeza created

    Hi,

    I tested my assumption about TransactionScope.Suppress and I was correct. The inner scope doesn't wait for the outer scope to complete before the changes show in the database. Please see the code below.

     using (var outerScope = new TransactionScope(TransactionScopeOption.Suppress))
                {
                    var outerConnection = new SqlConnection("Server=localhost;Database=Test;Trusted_Connection=true;Integrated Security=True;");
                    outerConnection.Open();
    
                    var command = outerConnection.CreateCommand();
                    command.CommandText = "SELECT * FROM dbo.ResearchProjects";
                    command.ExecuteNonQuery();
    
                    using (var innerScope = new TransactionScope(TransactionScopeOption.Required))
                    {
                        var innerCommand = outerConnection.CreateCommand();
                        command.CommandText = $"INSERT INTO dbo.ResearchProjects(ID) VALUES('{Guid.NewGuid()}');";
                        command.ExecuteNonQuery();
    
                        innerScope.Complete();
                    }
    
                    outerScope.Complete();
                }
    

    I still think UnitOfWork in ABP should work the same way.

    I appreciate that it currently does not work this way, so my question is how can I disable UnitOfWork interceptor for a specific ApplicationService. I can't use [UnitOfWork(IsDisabled=true)] because this ApplicationService gets called by other ApplicationServices.

    Thanks,

  • User Avatar
    0
    maliming created
    Support Team

    hi @Siyeza

    I think net framework may not have this problem.

    https://github.com/aspnetboilerplate/aspnetboilerplate/issues/1706

    I will review this design.

  • User Avatar
    0
    hikalkan created
    Support Team

    Hi @Siyeza,

    Commiting the Inner required transaction should not depend on the surrounding supporess.

    However, when I check the source code:

    https://github.com/aspnetboilerplate/aspnetboilerplate/blob/dev/src/Abp/Domain/Uow/UnitOfWorkManager.cs#L47

    I see that the behaviour is exactly like you experienced.

    This should be something like if (options.Scope == TransactionScopeOption.Required && outerUow != null && outerUow.Scope != TransactionScopeOption.Suppress)

    @maliming can you also check it and fix if you agree on that.

  • User Avatar
    0
    maliming created
    Support Team

    Solved by the PR below. https://github.com/aspnetboilerplate/aspnetboilerplate/pull/5689 https://github.com/aspnetboilerplate/aspnetboilerplate/pull/5688

  • User Avatar
    0
    Siyeza created

    Thank you!