aspnetboilerplate icon indicating copy to clipboard operation
aspnetboilerplate copied to clipboard

Interceptor calling async method before invocation.Proceed() Problem

Open naazz opened this issue 5 years ago • 10 comments

Using 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

  1. Download the sample here https://github.com/naazz/InterceptAsync
  2. 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

  1. 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.AbpZeroCommonDbContext3.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

naazz avatar Mar 28 '19 20:03 naazz

I will download your project and try it out.

maliming avatar Mar 29 '19 01:03 maliming

You must first call invocation.Proceed() and then await. Then don't call invocation.Proceed() again.

Please refer to: https://aspnetboilerplate.com/Pages/Documents/Articles/Aspect-Oriented-Programming-using-Interceptors/index.html#ArticleInterceptAsync

maliming avatar Mar 29 '19 02:03 maliming

Thank for your help. I read that article many times but it's not the same scenario at all. But i find this inside the article :

First of all, I could not find a way of executing async code before invocation.Proceed(). Because Castle Windsor does not support async naturally (other IOC managers also don't support as I know). So, if you need to run code before the actual method execution, do it synchronously. If you find a way of it, please share your solution as comment to this article.

So as i understand it was not possible to execute async code before invocation.proceed() before. Is it still true?

Could you be more precise about: You must first call invocation.Proceed() and then await. Then don't call invocation.Proceed() again. I should not call invocation.Proceed() in the second interceptor?

naazz avatar Mar 29 '19 17:03 naazz

Finally seem's to be fixed by castleproject/Core#443 in Castle.Core. @hikalkan could a quick realease be provided including that bug fix when Castle.Core release it?

naazz avatar Mar 29 '19 18:03 naazz

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:

First of all, I could not find a way of executing async code before invocation.Proceed().

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).

stakx avatar Mar 29 '19 18:03 stakx

Thanks a lot @stakx for informing us 😄.

ismcagdas avatar Apr 01 '19 12:04 ismcagdas

Just to add to the comment from @stakx we're looking to incorporate the soon to be released changes to Castle.Core into https://github.com/JSkimming/Castle.Core.AsyncInterceptor very soon, I have a Work In Progress pull request JSkimming/Castle.Core.AsyncInterceptor#54 on the go at the moment.

JSkimming avatar Apr 03 '19 21:04 JSkimming

FYI Castle.Core 4.4.0 (which includes the changes mentioned above) has been published on NuGet today.

stakx avatar Apr 05 '19 05:04 stakx

Thanks a lot @stakx this is a very good feature. I already implemented it for ABP vNext. Will work on this for this project too.

hikalkan avatar Apr 07 '19 19:04 hikalkan

This is work in progress on https://github.com/aspnetboilerplate/aspnetboilerplate/tree/pr/4401 branch. Currently there are some deadlocks and we will work on that in a few weeks.

ismcagdas avatar Jun 26 '19 06:06 ismcagdas