Interceptor calling async method before invocation.Proceed() Problem
See original GitHub issueUsing latest .net core angular all in one solution
I create 2 interceptors to do some stuff before a call to an appservice by calling an async methods.
- ValidationInterceptor: Call an async method to do some business logic and throw error if validation not pass and the continue execution
- PreTreatmentInterceptor: Executed after the validationinterceptor do some treatment before the appservice is called and receive the appservice dto in parameter and then continue execution
In my step to reproduce it i call /api/services/app/Test/Delete and everything work fine only for the first call, the call stack goes like this :
- Call to /api/services/app/Test/Delete with Id X
- ValidationInterceptor intercept the request and call TestAppServiceValidator.DeleteValidation
- DeleteValidation check that an entity exist with id received in parameter
- ValidationInterceptor does invocation.Proceed()
- PreTreatmentInteceptor intercept it and call TestAppServicePreTreatmentExecutor.PreTreatment_Delete
- PreTreatment_Delete delete related entity of entity “Test”
- PreTreatmentInteceptor does invocation.Proceed()
- TestAppService.Delete is called and delete the entity with id X
The problem
- After that i try a second call and im keep getting internalservererror.
- If i restart the solution the first call goes well and all subsequent give internalservererror.
- But if i remove async and task and switch method to sync inside TestAppServiceValidator and TestAppServicePreTreatmentExecutor everything works fine.
The logs are included in my sample repo bellow in the step to reproduce
I hope my explanation are clear enough. Thx in advance for any further help.
TestAppServiceValidator called by ValidationInterceptor
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Abp.Domain.Repositories;
using Abp.Runtime.Validation;
using Interceptors.Tests.Dtos;
using Microsoft.EntityFrameworkCore;
namespace Interceptors.Tests.Validations
{
public class TestAppServiceValidator : ITestAppServiceValidator
{
private readonly IRepository<Test> _repository;
public TestAppServiceValidator(IRepository<Test> pRepository)
{
_repository = pRepository;
}
public async Task DeleteValidation(DeleteIn pRequest)
{
if (!await _repository.GetAll().AnyAsync(a => a.Id == pRequest.Id))
{
throw new AbpValidationException("Unexisting entity");
}
}
//public void DeleteValidation(DeleteIn pRequest)
//{
// if (!_repository.GetAll().Any(a => a.Id == pRequest.Id))
// {
// throw new AbpValidationException("Unexisting entity");
// }
//}
}
}
TestAppServicePreTreatmentExecutor called by PostTreatmentInterceptor
using Interceptors.Interceptors.PreTreatment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Abp.Domain.Repositories;
using Interceptors.Tests.Dtos;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace Interceptors.Tests.PreTreatment
{
public class TestAppServicePreTreatmentExecutor : ITestAppServicePreTreatmentExecutor
{
public IRepository<TestPreTreatment> _repository;
public TestAppServicePreTreatmentExecutor(IRepository<TestPreTreatment> pRepository)
{
this._repository = pRepository;
}
public async Task PreTreatment_Delete(DeleteIn pRequest)//async
{
List<TestPreTreatment> list = await this._repository.GetAll().Where(w => w.TestId == pRequest.Id).ToListAsync();
foreach (TestPreTreatment testPreTreatment in list)
{
await this._repository.DeleteAsync(testPreTreatment);
}
}
//public void PreTreatment_Delete(DeleteIn pRequest)//async
//{
// List<TestPreTreatment> list = this._repository.GetAll().Where(w => w.TestId == pRequest.Id).ToList();
// foreach (TestPreTreatment testPreTreatment in list)
// {
// this._repository.Delete(testPreTreatment);
// }
//}
}
}
TestAppService
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Abp.Domain.Repositories;
using Interceptors.Interceptors.PreTreatment;
using Interceptors.Tests.Dtos;
namespace Interceptors.Tests
{
public class TestAppService : ITestAppService
{
private readonly IRepository<Test> _repository;
public TestAppService(IRepository<Test> pRepository)
{
_repository = pRepository;
}
[UxPreTreatment]
public virtual async Task Delete(DeleteIn pRequest)
{
await this._repository.DeleteAsync(w => w.Id == pRequest.Id);
}
}
}
Validation Interceptor
using Abp.Dependency;
using Castle.Core.Logging;
using Castle.DynamicProxy;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Interceptors.Interceptors.Validation
{
public class ValidationInterceptor : IInterceptor
{
private readonly IIocResolver _iocResolver;
public ILogger Logger { get; set; }
public ValidationInterceptor(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
Logger = NullLogger.Instance;
}
private bool ShouldIntercept(IInvocation invocation)
{
return invocation.MethodInvocationTarget.GetCustomAttribute<UxValidationDisabledAttribute>() == null;
}
public void Intercept(IInvocation invocation)
{
if (ShouldIntercept(invocation))
{
ValidateBeforeProceeding(invocation);
}
else
{
invocation.Proceed();
}
}
private void ValidateBeforeProceeding(IInvocation invocation)
{
IBaseValidatedAppService appService = invocation.InvocationTarget as IBaseValidatedAppService;
string assemblyName = invocation.InvocationTarget.GetType().BaseType.Assembly.ManifestModule.Name.Replace(".dll", ".");
string validatorName = "I" + appService.GetType().BaseType.Name + "Validator";
TypeResolver typeResolver = _iocResolver.Resolve<TypeResolver>();
Type validatorInterfaceType = typeResolver[assemblyName + validatorName];
if (validatorInterfaceType is null)
return;
IBaseValidator baseValidator = _iocResolver.Resolve(validatorInterfaceType) as IBaseValidator;
Type validatorType = baseValidator.GetType();
string methodName = invocation.MethodInvocationTarget.Name + "Validation";
MethodInfo method = validatorType.GetMethod(methodName);
if (method != null)
{
try
{
if (InternalAsyncHelper.IsAsyncMethod(method))
{
var returnValue = method.Invoke(baseValidator, invocation.Arguments);
if (method.ReturnType == typeof(Task))
{
returnValue = InternalAsyncHelper.AwaitTaskWithFinally(
(Task)returnValue,
ex =>
{
invocation.Proceed();
});
}
else //Task<TResult>
{
returnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult(
method.ReturnType.GenericTypeArguments[0],
returnValue,
ex =>
{
invocation.Proceed();
});
}
}
else
{
invocation.Proceed();
}
}
catch (Exception ex)
{
throw ex.InnerException;
}
}
else
{
}
}
}
}
PreTreatmentInterceptor
using Abp.Dependency;
using Castle.Core.Logging;
using Castle.DynamicProxy;
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Interceptors.Interceptors.PreTreatment
{
public class PreTreatmentInterceptor : IInterceptor
{
private readonly IIocResolver _iocResolver;
public ILogger Logger { get; set; }
public PreTreatmentInterceptor(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
Logger = NullLogger.Instance;
}
private bool ShouldIntercept(IInvocation invocation)
{
return invocation.MethodInvocationTarget.GetCustomAttribute<UxPreTreatment>() != null;
}
public void Intercept(IInvocation invocation)
{
if (ShouldIntercept(invocation))
{
PreTreatmentBeforeProceeding(invocation);
}
else
{
invocation.Proceed();
}
}
private void PreTreatmentBeforeProceeding(IInvocation invocation)
{
IPreTreatmentAppService appService = invocation.InvocationTarget as IPreTreatmentAppService;
string assemblyName = invocation.InvocationTarget.GetType().BaseType.Assembly.ManifestModule.Name.Replace(".dll", ".");
string preTreatmentExecutorName = "I" + appService.GetType().BaseType.Name + "PreTreatmentExecutor";
TypeResolver typeResolver = _iocResolver.Resolve<TypeResolver>();
Type preTreatmentExecutorInterfaceType = typeResolver[assemblyName + preTreatmentExecutorName];
if (preTreatmentExecutorInterfaceType is null)
return;
IPreTreatmentExecutor preTreatmentExecutor = _iocResolver.Resolve(preTreatmentExecutorInterfaceType) as IPreTreatmentExecutor;
Type preTreatmentExecutorType = preTreatmentExecutor.GetType();
string methodName = "PreTreatment_" + invocation.MethodInvocationTarget.Name;
MethodInfo method = preTreatmentExecutorType.GetMethod(methodName);
var request = invocation.Arguments[0];
if (method != null)
{
try
{
var returnValue = method.Invoke(preTreatmentExecutor, invocation.Arguments);
if (InternalAsyncHelper.IsAsyncMethod(method))
{
//Wait task execution and modify return value
if (method.ReturnType == typeof(Task))
{
invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithFinally(
(Task)returnValue,
ex =>
{
invocation.Proceed();
});
}
else //Task<TResult>
{
invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithFinallyAndGetResult(
method.ReturnType.GenericTypeArguments[0],
returnValue,
ex =>
{
invocation.Proceed();
});
}
}
else
{
invocation.Proceed();
}
}
catch (Exception ex)
{
throw ex.InnerException;
}
}
}
}
}
Steps to reproduce
- Download the sample here https://github.com/naazz/InterceptAsync
- Use the following script to add some records
INSERT INTO Tests(Props1, Props2) VALUES(‘Test1’, ‘Test2’)
INSERT INTO TestPreTreatments(Props1, TestId) SELECT ‘Test1’, Id FROM Tests
- Using swagger interface try the following request /api/services/app/Test/Delete with the id on of the record you previously inserted with the scripts. Stacktrace
INFO 2019-03-28 15:29:42,041 [32 ] soft.AspNetCore.Hosting.Internal.WebHost - Request starting HTTP/1.1 DELETE http://localhost:23000/api/services/app/Test/Delete?Id=13
INFO 2019-03-28 15:29:42,044 [32 ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution failed. INFO 2019-03-28 15:29:42,046 [32 ] pNetCore.Cors.Infrastructure.CorsService - Request origin http://localhost:23000 does not have permission to access the resource. INFO 2019-03-28 15:29:42,055 [32 ] ore.Mvc.Internal.ControllerActionInvoker - Route matched with {area = “app”, action = “Delete”, controller = “Test”}. Executing action Interceptors.Tests.TestAppService.Delete (Interceptors.Application) INFO 2019-03-28 15:29:42,057 [32 ] pNetCore.Cors.Infrastructure.CorsService - CORS policy execution failed. INFO 2019-03-28 15:29:42,059 [32 ] pNetCore.Cors.Infrastructure.CorsService - Request origin http://localhost:23000 does not have permission to access the resource. INFO 2019-03-28 15:29:42,066 [32 ] ore.Mvc.Internal.ControllerActionInvoker - Executing action method Interceptors.Tests.TestAppService.Delete (Interceptors.Application) with arguments (Interceptors.Tests.Dtos.DeleteIn) - Validation state: Valid ERROR 2019-03-28 15:29:43,843 [36 ] Mvc.ExceptionHandling.AbpExceptionFilter - A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe. This could also be caused by a nested query being evaluated on the client, if this is the case rewrite the query avoiding nested invocations. System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe. This could also be caused by a nested query being evaluated on the client, if this is the case rewrite the query avoiding nested invocations. at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection() at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList1 entriesToSave) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess) at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess) at Abp.EntityFrameworkCore.AbpDbContext.SaveChanges() in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\AbpDbContext.cs:line 208 at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext
3.SaveChanges() in D:\Github\aspnetboilerplate\src\Abp.ZeroCore.EntityFrameworkCore\Zero\EntityFrameworkCore\AbpZeroCommonDbContext.cs:line 159 at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesInDbContext(DbContext dbContext) in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 163 at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChanges() in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 60 at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.CompleteUow() in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 77 at Abp.Domain.Uow.UnitOfWorkBase.Complete() in D:\Github\aspnetboilerplate\src\Abp\Domain\Uow\UnitOfWorkBase.cs:line 256 at Abp.Auditing.AuditingHelper.Save(AuditInfo auditInfo) in D:\Github\aspnetboilerplate\src\Abp\Auditing\AuditingHelper.cs:line 135 at Abp.Auditing.AuditingInterceptor.SaveAuditInfo(AuditInfo auditInfo, Stopwatch stopwatch, Exception exception, Task task) in D:\Github\aspnetboilerplate\src\Abp\Auditing\AuditingInterceptor.cs:line 111 at Abp.Auditing.AuditingInterceptor.<>c__DisplayClass6_0.<PerformAsyncAuditing>b__0(Exception exception) in D:\Github\aspnetboilerplate\src\Abp\Auditing\AuditingInterceptor.cs:line 90 at Abp.Threading.InternalAsyncHelper.AwaitTaskWithFinally(Task actualReturnValue, Action`1 finalAction) in D:\Github\aspnetboilerplate\src\Abp\Threading\InternalAsyncHelper.cs:line 24 at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync() INFO 2019-03-28 15:29:59,760 [36 ] .Mvc.Infrastructure.ObjectResultExecutor - Executing ObjectResult, writing value of type ‘Abp.Web.Models.AjaxResponse’. INFO 2019-03-28 15:29:59,769 [36 ] ore.Mvc.Internal.ControllerActionInvoker - Executed action Interceptors.Tests.TestAppService.Delete (Interceptors.Application) in 17711.8481ms INFO 2019-03-28 15:29:59,772 [36 ] soft.AspNetCore.Hosting.Internal.WebHost - Request finished in 17731.019ms 500 application/json; charset=utf-8
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:10 (7 by maintainers)
Top GitHub Comments
FYI Castle.Core 4.4.0 (which includes the changes mentioned above) has been published on NuGet today.
Hi aspnetboilerplace project! Hailing from Castle.Core for a quick visit. A quick comment regarding https://aspnetboilerplate.com/Pages/Documents/Articles/Aspect-Oriented-Programming-using-Interceptors/index.html#ArticleInterceptAsync:
We’ve finally merged a solution to enable
Proceed
-after-await
in interceptors: see https://github.com/castleproject/Core/pull/439, as well as some new documentation that describes the problem and the proposed solution. This will become available in the next release of Castle.Core (shouldn’t be too long).