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

UnitOfWork and Async calls - Abp Version 7.0.0 #6927

Open
pawelignaczak opened this issue Apr 3, 2024 · 10 comments
Open

UnitOfWork and Async calls - Abp Version 7.0.0 #6927

pawelignaczak opened this issue Apr 3, 2024 · 10 comments

Comments

@pawelignaczak
Copy link

pawelignaczak commented Apr 3, 2024

Abp Version 7.0.0

What are the best practices for UnityOfWork lifecycle in frames of async request between services? We have some issues with UoW context getting lost in that scenario. If I have an event served as async like :

    [UnitOfWork]
public async Task<T> InvokeWithUnitOfWorkAsync<T>(Func<Task<T>> serviceCall)
{
	using (var uow = _unitOfWorkManager.Begin(TransactionScopeOption.RequiresNew))
	{
		using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
		{
			try
			{
				var result = await serviceCall();
				await uow.CompleteAsync();
				return result;
			}
			catch (Exception ex)
			{
				return default(T);
			}
		}
	}

}
and my handler is calling a few other services like:

           [UnitOfWork]
	private async Task HandleGroupDirectCall(){
	    var interactionDto = await _interactionAppService.CreateAsync(caller); 
	}

and further:

[AllowAnonymous]
[UnitOfWork]
public override async Task<InteractionDto> CreateAsync(CreateInteractionDto input)
{
	var tenant= await _tenantKeyAppService.GetLastKeyId((long)input.TenantId);
	//<-here UoW scope ends , if i remove this like 
	//CreateAsync will know UoW but do not know the objects I creating
	
	var interdto = await base.CreateAsync(input);//<-here i get a crash due to lack of UoW
}

Seems like any await async call is causing drop of UoW context even if method is decorated with [UnitOfWork]

@pawelignaczak pawelignaczak changed the title UnitOfWork and Async calls UnitOfWork and Async calls - Abp Version 7.0.0 Apr 3, 2024
@ismcagdas
Copy link
Member

@pawelignaczak could you share your ABP version and format the code in the issue properly ?

@pawelignaczak
Copy link
Author

Done

@ismcagdas
Copy link
Member

Thanks. Is it possible for you to prepare a sample project with this problem ?

@pawelignaczak
Copy link
Author

That would be difficult as the current project is big. Basically We get an event from remote server that triggers an async request with a new UoW: _unitOfWorkManager.Begin(TransactionScopeOption.RequiresNew).
The a chain of async requests are called. My latest findigs is that the any async bolerplate repo calls like:

                var     interdto = await base.CreateAsync(input);

or
var list=_interactionsRepository.GetAllListAsync()
or
var dataReader = await command.ExecuteReaderAsync();
if i use say :
var dataReader = command.ExecuteReader();
Then it does not happen.
But i cannot avoid using:
var interdto = await base.CreateAsync(input);

@pawelignaczak
Copy link
Author

In fact i randomly get below error when try to call :
var interdto = await base.CreateAsync(input);

System.InvalidOperationException: „A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.”

@ismcagdas
Copy link
Member

ismcagdas commented Apr 5, 2024

Could you share your startup.cs ? Especially services.AddAbp block.

@pawelignaczak
Copy link
Author

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Castle.Facilities.Logging;
using Abp.AspNetCore;
using Abp.AspNetCore.Mvc.Antiforgery;
using Abp.Castle.Logging.Log4Net;
using AseeVT.Authentication.JwtBearer;
using AseeVT.Configuration;
using AseeVT.Identity;
using AseeVT.Web.Resources;
using Abp.AspNetCore.SignalR.Hubs;
using Abp.Dependency;
using Abp.Json;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json.Serialization;
using AseeVT.Hubs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Identity;
using log4net;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using AseeVT.Web.Models.AzureKeyVault;
using Azure.Identity;
using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;

namespace AseeVT.Web.Startup
{
public class Startup
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IConfigurationRoot _appConfiguration;

    public Startup(IWebHostEnvironment env)
    {
        _hostingEnvironment = env;
        _appConfiguration = env.GetAppConfiguration();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var azureKeyVaultConfig = _appConfiguration.GetSection("AzureKeyVault").Get<AzureKeyVaultConfig>();
        
        var clientSecretCredential = new ClientSecretCredential(
                  azureKeyVaultConfig.TenantId, 
                  azureKeyVaultConfig.ClientId, 
                  azureKeyVaultConfig.ClientSecret ); 

        SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential);
        SqlConnection.RegisterColumnEncryptionKeyStoreProviders(
            customProviders: new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>(comparer: StringComparer.OrdinalIgnoreCase)
            {
                { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider }
            });

        // MVC
        services.AddControllersWithViews(
                options =>
                {
                    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
                    options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute());
                }
            )
            .AddRazorRuntimeCompilation()
            .AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.ContractResolver = new AbpMvcContractResolver(IocManager.Instance)
                {
                    NamingStrategy = new CamelCaseNamingStrategy()
                };
            });

        //services.ConfigureApplicationCookie(o =>
        //{
        //    o.ExpireTimeSpan = TimeSpan.FromHours(8);
        //    o.SlidingExpiration = false;
        //});

        services.AddSingleton<IPostConfigureOptions<SecurityStampValidatorOptions>, ConfigureSecurityStampValidatorOptionsService>();

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            options.ExpireTimeSpan = TimeSpan.FromMinutes(240);
            options.SlidingExpiration = false;
            options.AccessDeniedPath = "/Forbidden/";
        });

        IdentityRegistrar.Register(services);
        AuthConfigurer.Configure(services, _appConfiguration);

        services.AddScoped<IWebResourceManager, WebResourceManager>();

        var options = new JsonSerializerOptions
        {
            ReferenceHandler = ReferenceHandler.Preserve
        };

        services.AddSignalR(o =>
        {
            o.EnableDetailedErrors = true;

        }).AddJsonProtocol(options =>
        {
            options.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
        });

        // Configure Abp and Dependency Injection
        services.AddAbpWithoutCreatingServiceProvider<AseeVTWebMvcModule>(
            // Configure Log4Net logging
            options => options.IocManager.IocContainer.AddFacility<LoggingFacility>(
                f => f.UseAbpLog4Net().WithConfig(
                    _hostingEnvironment.IsDevelopment()
                        ? "log4net.config"
                        : "log4net.Production.config"
                    )
            )
        );

    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        //app.UsePathBase(new PathString("/app"));

        app.UseAbp(); // Initializes ABP framework.

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            //app.UseExceptionHandler("/Error");
        }
        else
        {
            app.UseExceptionHandler("/Error");
        }

        app.Use(async (context, next) =>
       {
           LogicalThreadContext.Properties["CorrelationId"] = context.TraceIdentifier;

           await next.Invoke();
       });

        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();

        app.UseJwtTokenMiddleware();

        app.UseAuthorization();

        //app.UseSession();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<AbpCommonHub>("/signalr");
            endpoints.MapHub<AcdHub>("/signalr-acd");
            endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
            endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}");
        });
    }
}

}

@ismcagdas
Copy link
Member

Thanks. This seems fine. Is it possible that your code has any async over sync usages ? If you are using Jetbrains Rider, you can find such usages in your code. If not, you can try to remove TransactionScopeOption.RequiresNew if there is no special reason to use it.

@pawelignaczak
Copy link
Author

Well that's the point. We receive TCP events from other system and create an asyn event from it that process some actions on services. At the same time the services can be accessed from say frontend or SignalR.

@ismcagdas
Copy link
Member

I mean only the code in your app, not how it is called outside from your app.

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

No branches or pull requests

2 participants