0

I have an Aspect class that uses the MethodInterception class. When I create a throw like below in this class, ExceptionMiddleware does not handle, why?

I am using Castle.DynamicProxy for MethodInterception. Since Castle.DynamicProxy has a working problem in async methods, I solved my problem by using the extension 'Castle.Core.AsyncInterceptor'. But now the problem is that the throw exception from an async interceptor is not handled by the exception middleware. If I don't define the onBefore method as async, the exception middleware works, but I run await method in onbefore, so it's necessary.

public class ExceptionMiddleware
{
    private RequestDelegate _next;

    public ExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception e)
        {

            await HandleExceptionAsync(httpContext, e);
        }
    }

    private Task HandleExceptionAsync(HttpContext httpContext, Exception e)
    {
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        string message = "Internal Server Error";

        if (e.GetType() == typeof(UnauthorizedAccessException))
        {
            message = e.Message;
            httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        }

        return httpContext.Response.WriteAsync(new ErrorDetails
        {
            StatusCode = httpContext.Response.StatusCode,
            Message = message
        }.ToString());
    }
}

SecuredScopeAspect

public class SecuredOperation : MethodInterception
{
    private string[] _roles;
    private IHttpContextAccessor _httpcontextAccessor;

    public SecuredOperation()
    {
        _httpcontextAccessor = ServiceTool.ServiceProvider.GetService<IHttpContextAccessor>();
    }
    public SecuredOperation(string roles)
    {
        _roles = roles.Split(',');
        _httpcontextAccessor = ServiceTool.ServiceProvider.GetService<IHttpContextAccessor>();

    }

    protected override async void OnBefore(IInvocation invocation)
    {
        if (_httpcontextAccessor.HttpContext.User.Identity.IsAuthenticated)
        {
            if (_roles != null)
            {
                //some await proccess
                 const result = await _fga.check();
                 if(result != null) return;

                 throw new MemberAccessException(AuthMessages.AuthorizationForbidden);
            }
            return;
        }

        throw new UnauthorizedAccessException(AuthMessages.AuthorizationDenied);
    }
}

MethodInterception.cs

 public abstract class MethodInterception : MethodInterceptionBaseAttribute
    {
        protected virtual void OnBefore(IInvocation invocation)
        {

        }
        protected virtual void OnAfter(IInvocation invocation)
        {

        }
        protected virtual void OnException(IInvocation invocation, System.Exception e)
        {

        }
        protected virtual void OnSuccess(IInvocation invocation)
        {

        }


        public override void InterceptSynchronous(IInvocation invocation)
        {
            var isSuccess = true;
            OnBefore(invocation);
            
            try
            {
                invocation.Proceed();
            }
            catch (Exception e)
            {
                isSuccess = false;
                OnException(invocation, e);
                throw;
            }
            finally
            {
                if (isSuccess)
                {
                    OnSuccess(invocation);
                }
            }
            OnAfter(invocation);
        }

        public override void InterceptAsynchronous(IInvocation invocation)
        {
            invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
        }

        protected async Task InternalInterceptAsynchronous(IInvocation invocation)
        {
           

            var isSuccess = true;
            OnBefore(invocation);
            try
            {
                invocation.Proceed();
                var task = (Task)invocation.ReturnValue;
                await task;
            }
            catch (Exception e)
            {
                isSuccess = false;
                OnException(invocation, e);
                throw;
            }
            finally
            {
                if (isSuccess)
                {
                    OnSuccess(invocation);
                }
            }
            OnAfter(invocation);
        }

        public override void InterceptAsynchronous<TResult>(IInvocation invocation)
        {
            invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
        }

        protected async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
        {


            TResult result;
            var isSuccess = true;
            OnBefore(invocation);
            try
            {
                invocation.Proceed();
                var task = (Task<TResult>)invocation.ReturnValue;
                result = await task;
            }
            catch (Exception e)
            {
                isSuccess = false;
                OnException(invocation, e);
                throw;
            }
            finally
            {
                if (isSuccess)
                {
                    OnSuccess(invocation);
                }
            }
            OnAfter(invocation);
            return result;

        }

    }
        }
CodAvo
  • 183
  • 1
  • 2
  • 16
  • Did u try changing the `async void` to `async Task`? – thanzeel Feb 09 '23 at 15:41
  • Yeah I did, But this time, there is a case of whether the method called in the method Interception class is asynchronous or not. If the method that the service calls is a synchronous method, it creates a problem. You can review my MethodInterception class above. @thanzeel – CodAvo Feb 09 '23 at 16:05
  • seems like you can just stick to `async` methods only and get rid of the `synchronous` methods since you have full control ryt – thanzeel Feb 09 '23 at 16:13
  • How? I removed the async tag from the OnBefore method, created a Task method called CheckAsync in the same class and placed the await _fga.check() operation there. I added this line inside the OnBefore method 'bool result = CheckAsync().Result;' and I solved the problem, but I don't know if it will cause problems in the long run. @thanzeel – CodAvo Feb 09 '23 at 16:24
  • dont use `Result`, this has major impacts. your `protected override async void OnBefore(IInvocation invocation)` works like fire and forgets, so u dont get the `Task`. u need to change it to `protected override async Task OnBefore(IInvocation invocation)` – thanzeel Feb 09 '23 at 16:27
  • If I make changes as you said, this time I will be having problems with the InterceptSynchronous method. @thanzeel – CodAvo Feb 09 '23 at 17:06
  • may be you can create a minimal repository and share so we can have a look. your problem is not clear enough – thanzeel Feb 09 '23 at 17:07
  • Creating a minimal repo may take my time, so let me briefly describe my scenario. When every http request reaches my services, it is first met by interceptors, the first interceptor it encounters is the SecuredOperation interceptor. In this interceptor, I control the user's authority by connecting to the OpenFGA API. The SecuredOperation interceptor is a MethodInterception. If the interrupted method is a normal synchronous method, the InterceptSynchronous method works. If it is asynchronous method, my InterceptAsynchronous or InterceptAsynchronous methods will work. @thanzeel – CodAvo Feb 09 '23 at 17:14
  • is it not possible to write a middleware for this? since it seems like authentication – thanzeel Feb 09 '23 at 17:15
  • not authentication, authorization. – CodAvo Feb 09 '23 at 17:18
  • OpenFGA undertakes the authorization processes as middleware. @thanzeel – CodAvo Feb 09 '23 at 17:18
  • Why not use it then? – thanzeel Feb 09 '23 at 17:21
  • I'm already using OpenFGA? @thanzeel – CodAvo Feb 09 '23 at 17:22
  • Our topic is not openfga, our topic is to run an asynchronous method within a synchronous method. @thanzeel – CodAvo Feb 09 '23 at 17:23
  • Would this help https://stackoverflow.com/questions/9343594/how-to-call-asynchronous-method-from-synchronous-method-in-c – thanzeel Feb 09 '23 at 17:27
  • There are many methods, but they are generally not recommended. Right now, I have the idea of making all services asynchronous, otherwise I can't solve the method interceptor problem. @thanzeel – CodAvo Feb 09 '23 at 20:31
  • Thats is what i told you in the previous comments, make all the methods async – thanzeel Feb 10 '23 at 00:43

1 Answers1

0

This is your problem: async void. void is not a natural return type for asynchronous code, and it has very surprising exception handling semantics in particular.

So, OnBefore and friends have to become async Task instead.

Then, this causes another problem: how to intercept synchronous calls? Blocking on asynchronous code is possible in ASP.NET Core (it doesn't cause deadlocks), but it's not an ideal solution because it wastes threads. [Links are to my blog].

Welcome to the complexities of asynchronous-and-synchronous interception. It's not a pretty world.

Your options are:

  1. Only intercept asynchronous methods. I don't know how DynamicProxy works in this case; the last time I used it they didn't support intercepting asynchronous methods with asynchronous pre-work at all. So it may be that it just ignores synchronous methods, which leaves you with a rather nasty pit-of-failure. Alternatively, you could always fail synchronous methods, so at least you would have some indication it's being used incorrectly (even if that indication is at runtime in production...)
  2. Make all On* methods asynchronous (return Task instead of void), and just block on them for the synchronous interceptions.
  3. Force/allow all your derived interception types to support both synchronous and asynchronous implementations.

IMO (2) would be cleanest in terms of code maintainability. (2) would look like:

public abstract class MethodInterception : MethodInterceptionBaseAttribute
{
  protected virtual Task OnBefore(IInvocation invocation) => Task.CompletedTask;
  protected virtual Task OnAfter(IInvocation invocation) => Task.CompletedTask;
  protected virtual Task OnException(IInvocation invocation, System.Exception e) => Task.CompletedTask;
  protected virtual Task OnSuccess(IInvocation invocation) => Task.CompletedTask;

  public override void InterceptSynchronous(IInvocation invocation)
  {
    var isSuccess = true;
    OnBefore(invocation).GetAwaiter().GetResult();

    try
    {
      invocation.Proceed();
    }
    catch (Exception e)
    {
      isSuccess = false;
      OnException(invocation, e).GetAwaiter().GetResult();
      throw;
    }
    finally
    {
      if (isSuccess)
      {
        OnSuccess(invocation).GetAwaiter().GetResult();
      }
    }
    OnAfter(invocation).GetAwaiter().GetResult();
  }

  public override void InterceptAsynchronous(IInvocation invocation)
  {
    invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
  }

  protected async Task InternalInterceptAsynchronous(IInvocation invocation)
  {
    var isSuccess = true;
    await OnBefore(invocation);
    try
    {
      invocation.Proceed();
      var task = (Task)invocation.ReturnValue;
      await task;
    }
    catch (Exception e)
    {
      isSuccess = false;
      await OnException(invocation, e);
      throw;
    }
    finally
    {
      if (isSuccess)
      {
        await OnSuccess(invocation);
      }
    }
    await OnAfter(invocation);
  }

  ... // similarly for <TResult>
}

public class SecuredOperation : MethodInterception
{
  private string[] _roles;
  private IHttpContextAccessor _httpcontextAccessor;

  protected override async Task OnBefore(IInvocation invocation)
  {
    if (_httpcontextAccessor.HttpContext.User.Identity.IsAuthenticated)
    {
      if (_roles != null)
      {
        const result = await _fga.CheckAsync();
        if (result != null) return;
        throw new MemberAccessException(AuthMessages.AuthorizationForbidden);
      }
      return;
    }

    throw new UnauthorizedAccessException(AuthMessages.AuthorizationDenied);
  }
}

An example of (3) using the boolean argument pattern may look like this:

public abstract class MethodInterception : MethodInterceptionBaseAttribute
{
  protected virtual Task OnBefore(IInvocation invocation, bool sync) => Task.CompletedTask;
  protected virtual Task OnAfter(IInvocation invocation, bool sync) => Task.CompletedTask;
  protected virtual Task OnException(IInvocation invocation, System.Exception e, bool sync) => Task.CompletedTask;
  protected virtual Task OnSuccess(IInvocation invocation, bool sync) => Task.CompletedTask;

  public override void InterceptSynchronous(IInvocation invocation)
  {
    var isSuccess = true;
    OnBefore(invocation, sync: true).GetAwaiter().GetResult();

    try
    {
      invocation.Proceed();
    }
    catch (Exception e)
    {
      isSuccess = false;
      OnException(invocation, e, sync: true).GetAwaiter().GetResult();
      throw;
    }
    finally
    {
      if (isSuccess)
      {
        OnSuccess(invocation, sync: true).GetAwaiter().GetResult();
      }
    }
    OnAfter(invocation, sync: true).GetAwaiter().GetResult();
  }

  public override void InterceptAsynchronous(IInvocation invocation)
  {
    invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
  }

  protected async Task InternalInterceptAsynchronous(IInvocation invocation)
  {
    var isSuccess = true;
    await OnBefore(invocation, sync: false);
    try
    {
      invocation.Proceed();
      var task = (Task)invocation.ReturnValue;
      await task;
    }
    catch (Exception e)
    {
      isSuccess = false;
      await OnException(invocation, e, sync: false);
      throw;
    }
    finally
    {
      if (isSuccess)
      {
        await OnSuccess(invocation, sync: false);
      }
    }
    await OnAfter(invocation, async: false);
  }

  ... // similarly for <TResult>
}

public class SecuredOperation : MethodInterception
{
  private string[] _roles;
  private IHttpContextAccessor _httpcontextAccessor;

  protected override async Task OnBefore(IInvocation invocation, bool sync)
  {
    if (_httpcontextAccessor.HttpContext.User.Identity.IsAuthenticated)
    {
      if (_roles != null)
      {
#if (_fga supports synchronous calls)
        const result = sync ? _fga.Check() : await _fga.CheckAsync();
#else
        const result = sync ? _fga.CheckAsync().GetAwaiter().GetResult() : await _fga.CheckAsync();
#endif
        if (result != null) return;
        throw new MemberAccessException(AuthMessages.AuthorizationForbidden);
      }
      return;
    }

    throw new UnauthorizedAccessException(AuthMessages.AuthorizationDenied);
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you for your response, Question 1 = As a solution, I can make the methods of all my services asynchronous. Is this approach correct? Question 2 = Is there an interception library that supports both synchronous and asynchronous that I can use as an alternative to Dynamix Proxy? Question 3 = What is the downside of your answer? – CodAvo Feb 11 '23 at 11:06
  • 1. Yes, if they are all asynchronous. 2. DybamicProxy supports both asynchronous and synchronous just fine; the difficulty comes because it can't *change* synchronous to asynchronous, which of course wouldn't be possible for any interception library. 3. Option 1 ignores synchronous methods; option 2 blocks threads; option 3 results in complex code. – Stephen Cleary Feb 11 '23 at 11:52
  • Thank you for your answer, I will implement your solution. I will convert the synchronous methods in all my api services to asynchronous structure. OpenFGA SDK do not provide or require synchronous solutions, so every time SecuredOperation comes to the interceptor, it has to run asynchronously. – CodAvo Feb 11 '23 at 12:16
  • An infinite loop started to occur in the code you suggested. – CodAvo Feb 12 '23 at 21:14
  • I found solution in here -> https://github.com/castleproject/Core/blob/master/docs/dynamicproxy-async-interception.md#using-invocationproceed-in-combination-with-asyncawait – CodAvo Feb 13 '23 at 19:14
  • Use this as a proceed in asynchronous interceptors. var proceed = invocation.CaptureProceedInfo(); – CodAvo Feb 13 '23 at 19:15