Issue Description
I have an issue with my abp.io application (v9.1.0, .net 9.0) where my application service method is being called multiple times when a Kendo grid makes a single request to a page handler. The grid calls the page handler once, but the application service method (which takes some time to complete) gets called multiple times with different UnitOfWork IDs.
Sequence of Events
- Browser makes HTTP POST to PageHandler (/Files/Edt?handler=ReadRecords)
- Page Handler calls EdtFileAppService.GetListAsync()
- This method is then called multiple times, as shown in my logs
Attempted Solutions
- Applied
[UnitOfWork(IsDisabled = true)]
to both PageHandler and AppService method, it's ignored (I suspected it might be UOW related, but thinking it's more the API layer now) - Confirmed I've added no retry policy using
AddTransientHttpErrorPolicy
inAbpHttpClientBuilderOptions
>ProxyClientBuildActions
- Set long timeout in
AbpHttpClientBuilderOptions
>ProxyClientActions
- Created custom
IProxyHttpClientFactory
implementation with long timeout - Tried using GET instead of POST
Code Examples
Kendo Grid Definition (Simplified)
Html.Kendo().Grid<EdtFileViewModel>().Name("EdtFilesGrid")
.DataSource(ds => ds.Custom().Type("aspnetmvc-ajax")
.ServerFiltering(true)
.ServerPaging(true)
.ServerSorting(true)
.Schema(s => s.Aggregates("aggregateResults").Data("data").Errors("errors").Total("total").Model(m=>
{
m.Id(x => x.Id);
// Other field mappings...
}))
.Transport(t => t.Read(r => r.Url("/Files/Edt?handler=ReadRecords").Data("EdtFilesGrid_AdditionalData").Type(HttpVerbs.Post)))
.PageSize(10))
.Sortable()
.Pageable()
.Deferred()
.Render();
Application Service Method
public override async Task<PagedResultDto<EdtFileDto>> GetListAsync(EdtFilePagedAndSortedResultRequestDto input)
{
Logger.LogWarning("Method start: {FullName}.GetListAsync, Hash: {I}", GetType().FullName, GetHashCode());
var isProxy = GetType().Namespace?.Contains("Castle.Proxies") == true;
Logger.LogWarning("Is this object a proxy? {IsProxy}", isProxy);
Logger.LogWarning("UOW ID: {Guid}, IsDisabled attribute: {IsDisabled}",
CurrentUnitOfWork?.Id,
GetType().GetMethod(nameof(GetListAsync)).GetCustomAttribute<UnitOfWorkAttribute>()?.IsDisabled);
await Task.Delay(TimeSpan.FromSeconds(30)); // Added delay to demonstrate issue
// Additional code...
}
Logs
[2025-05-15 14:47:04.371] [Warning] () <CabMD.Edt.EdtFileAppService> Method start: "CabMD.Edt.EdtFileAppService".GetListAsync, Hash: 26433642
[2025-05-15 14:47:04.371] [Warning] () <CabMD.Edt.EdtFileAppService> Is this object a proxy? False
[2025-05-15 14:47:04.371] [Warning] () <CabMD.Edt.EdtFileAppService> UOW ID: 63bfe718-a534-4c2a-b86b-57c783f71873, IsDisabled attribute: null
[2025-05-15 14:47:17.011] [Warning] () <CabMD.Edt.EdtFileAppService> Method start: "CabMD.Edt.EdtFileAppService".GetListAsync, Hash: 24543723
[2025-05-15 14:47:17.011] [Warning] () <CabMD.Edt.EdtFileAppService> Is this object a proxy? False
[2025-05-15 14:47:17.011] [Warning] () <CabMD.Edt.EdtFileAppService> UOW ID: e9cd7595-b92e-442b-b0fb-c1676a812e0b, IsDisabled attribute: null
[2025-05-15 14:47:29.429] [Warning] () <CabMD.Edt.EdtFileAppService> Method start: "CabMD.Edt.EdtFileAppService".GetListAsync, Hash: 26575601
[2025-05-15 14:47:29.429] [Warning] () <CabMD.Edt.EdtFileAppService> Is this object a proxy? False
[2025-05-15 14:47:29.429] [Warning] () <CabMD.Edt.EdtFileAppService> UOW ID: 3cdba2e4-04d2-4f0c-8039-0c06e913d793, IsDisabled attribute: null
Additional Information
Our implementation is fitting ABP on top of an existing DB (coming from ASP.NET 4.8), so our custom DB layer makes repositories that then make the underlying calls to the DB (usually using stored procedures) - all of that works fine.
Based on the sequence diagram I've attached (which shows the flow from Browser → Razor Page Handler → Static HTTP Proxy → API Controller → Application Service), I believe the issue might be related to the Static HTTP Proxy layer, but I'm unsure what mechanism is causing these duplicate calls.
I'm 100% certain the Kendo grid is not making multiple requests. The issue occurs between the HTTP Proxy and the application service. Can you please advise on how to prevent these duplicate calls or identify what's causing them? I don't want to lock the app service method up with a semaphore, as we want many requests to be responded to concurrently and this is all but one example.
5 Answer(s)
-
0
Hello,
Do you see the request twice when you use the ABP DataGrid etc. instead of KendoUI or the request manually from Swagger? When I search the datagrid of Kendo UI on the internet, I think that this is due to the filter of KendoUI's datagrid.
See more:
- https://www.telerik.com/forums/kendo-grid-filter-posting-twice
- https://stackoverflow.com/questions/24068047/kendo-grid-making-duplicate-api-calls
- https://www.google.com/search?q=Kendo+grid+make+request+twice&oq=Kendo+grid+make+request+twice&gs_lcrp=EgZjaHJvbWUyCwgAEEUYChg5GKABMgkIARAhGAoYoAHSAQg3NDA5ajBqMagCALACAA&sourceid=chrome&ie=UTF-8
At first glance, I don't think it's ABP problem, so I want to make sure it's not KendoUI problem, so what have you done to test that it's not Kendo issue?
-
0
I'm 99% certain it has nothing to do with the actual client control (kendo). It's making a single call to the API, then the app service portion is being repeatedly called.
I only see a single rest call to the page handler, which is pending the entire time that I get stopped multiple times at the app service layer.
-
0
Hi,
If it is possible, can you send your solution to support@abp.io email address (with the ticket number - #9325). Regards.
-
0
It appears to be an issue with Aspire and ABP. Now that I'm pretty sure this is the issue, I'll be able to fix up my repro and send that to you.
Alternatively, your sample should have the same exact issue. The easiest way to see this is to have something in web call an app service where you either intentionally or unintentionally have code that takes long, like adding
await Task.Delay(TimeSpan.FromSeconds(30));
- then you should see the exact same issue.Once I understood where the Polly retry was being caused, I was able to modify the AddServiceDefaults() extension method like so:
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); builder.Services.AddServiceDiscovery(); builder.Services.ConfigureHttpClientDefaults(http => { // Turn on resilience by default http.AddStandardResilienceHandler(resilience => { // Set timeouts resilience.AttemptTimeout.Timeout = TimeSpan.FromMinutes(2); resilience.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(5); // Must be > attempt timeout // Configure retry (either set to 1+ or disable the strategy) resilience.Retry.MaxRetryAttempts = 1; // Minimum allowed value // Configure circuit breaker resilience.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(5); // Must be ≥ 2× attempt timeout }); // Turn on service discovery by default http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. // builder.Services.Configure<ServiceDiscoveryOptions>(options => // { // options.AllowedSchemes = ["https"]; // }); return builder; }
Specifically, the section
AddStandardResilienceHandler
is what needed to be modified.Web Log: [2025-05-27 16:36:40.353] [Warning] () <CabMD.Web.Pages.Files.EdtIndexModel> === PAGE HANDLER START === [2025-05-27 16:36:40.354] [Warning] () <CabMD.Web.Pages.Files.EdtIndexModel> CorrelationId: "66a388fe", Time: 16:36:40.354 [2025-05-27 16:36:40.354] [Warning] () <CabMD.Web.Pages.Files.EdtIndexModel> Request: Page=1, PageSize=10, Skip=0 [2025-05-27 16:37:10.917] [Warning] () <CabMD.Web.Pages.Files.EdtIndexModel> === PAGE HANDLER END === [2025-05-27 16:37:10.917] [Warning] () <CabMD.Web.Pages.Files.EdtIndexModel> CorrelationId: "66a388fe", ResultCount: 10 [2025-05-27 16:37:10.935] [Warning] () <CabMD.PerformanceDiagnosticMiddleware> Slow request: "POST" "/Files/Edt" ("files/edt") took 30798ms (exceeds tolerable threshold of 500ms) [Status: 200]
API Log: [2025-05-27 16:36:40.733] [Warning] () <CabMD.Controllers.Edt.EdtFileController> === API CONTROLLER START === [2025-05-27 16:36:40.733] [Warning] () <CabMD.Controllers.Edt.EdtFileController> RequestId: "6eca2ca5", Time: 16:36:40.733 [2025-05-27 16:36:40.734] [Warning] () <CabMD.Controllers.Edt.EdtFileController> HttpContext.TraceIdentifier: "0HNCTBOB4A9NI:00000026" [2025-05-27 16:36:40.734] [Warning] () <CabMD.Controllers.Edt.EdtFileController> Input: Skip=0, Max=10, Sorting=null [2025-05-27 16:36:40.736] [Warning] () <CabMD.Controllers.Edt.EdtFileController> Headers: "X-Requested-With=XMLHttpRequest" [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> === APP SERVICE START === [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> RequestId: "dfc9379f", CorrelationId: "no-correlation", Time: 16:36:40.745 [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> Method: "CabMD.Edt.EdtFileAppService".GetListAsync, Hash: 42651975 [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> Is Proxy: False [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> UOW ID: d451c0ca-00d6-4aeb-91d5-e175cf90ddf5, UOW IsActive: True [2025-05-27 16:36:40.745] [Warning] () <CabMD.Edt.EdtFileAppService> Input: Skip=0, Max=10 [2025-05-27 16:36:40.749] [Warning] () <CabMD.Edt.EdtFileAppService> Call Stack: "<GetListAsync>d__13.MoveNext -> AsyncMethodBuilderCore.Start -> EdtFileAppService.GetListAsync -> IReadOnlyAppService
4_GetListAsync_7.InvokeMethodOnTarget -> AbstractInvocation.Proceed -> ProceedInfo.Invoke -> <ProceedAsynchronous>d__14
1.MoveNext -> AsyncMethodBuilderCore.Start -> AsyncInterceptorBase.ProceedAsynchronous -> <ProceedAsync>d__7.MoveNext" [2025-05-27 16:37:10.831] [Warning] () <CabMD.Controllers.Edt.EdtFileController> === API CONTROLLER END === [2025-05-27 16:37:10.832] [Warning] () <CabMD.Controllers.Edt.EdtFileController> RequestId: "6eca2ca5", ResultCount: 10Generally, in the host apps - there is something similar to the following:
Log.Information("Starting web host."); var builder = WebApplication.CreateBuilder(args); builder.AddRedisClient("redis"); builder.AddRedisDistributedCache("redis"); builder.AddServiceDefaults(); // Bam, issue - if this is the default AddServiceDefaults and nothing modifies the resiliancy
-
0
Hi,
First of all, thank you very much for the information you have provided.
.NET Aspire provides opinionated templates. In the article, the ServiceDefaults project is created using dotnet commands without any additional customization. Since these templates are opinionated, some configurations—such as retry timeout durations—are pre-configured based on common scenarios.
However, these default settings may not suit your specific needs or certain endpoints. In such cases, you should customize the configuration accordingly. For instance, if you have non-idempotent methods, you might want to disable the retry mechanism altogether.
You can also disable retries specifically for POST methods using the following configuration:
httpClientBuilder.AddStandardResilienceHandler(options => { options.Retry.DisableFor(HttpMethod.Post); });
For more information: https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience?tabs=dotnet-cli#disable-retries-for-a-given-list-of-http-methods
I highly recommend reading this article on how you can fine-tune it: https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter