11

I have an ASP.NET MVC 2 application in which I am creating a custom action filter. This filter sits on the controllers in the application and verifies from the database whether that function is currently available.

Public Overrides Sub OnActionExecuting(ByVal filterContext As System.Web.Mvc.ActionExecutingContext)
  Try
    ' Check controller name against database.
    Dim controllerName = filterContext.Controller.GetType().Name
    controllerName = controllerName.Remove(controllerName.Length - 10)
    ' Look up availability.
    Dim available As Boolean = _coreService.GetControllerAvailability(controllerName)
    If Not available Then
      ' Redirect to unavailable notice.
      filterContext.Result = New RedirectResult("/Home/Unavailable/")
    End If
  Catch ex As Exception
    _eventLogger.LogWarning(ex, EventLogEntryType.Error)
    Throw
  End Try
End Sub

My problem is that depending on the action that has been requested I need to redirect the user to an action that returns either a view, partial views or JSON.

Given the ActionExecutingContext can I find out what the return type of the originally requested action is?

EDIT:

Ok, I'm getting closer but have another problem.

Public Overrides Sub OnActionExecuting(ByVal filterContext As System.Web.Mvc.ActionExecutingContext)
  Try
    ' Check controller name against database.
    Dim controllerName = filterContext.Controller.GetType().Name
    Dim shortName = controllerName.Remove(controllerName.Length - 10)
    ' Look up availability.
    Dim available As Boolean = _coreService.GetControllerAvailability(shortName)
    If Not available Then
      ' find out what type is expected to be returned
      Dim actionName As String = filterContext.ActionDescriptor.ActionName
      Dim controllerType = Type.GetType("Attenda.Stargate.Web." & controllerName)
      Dim actionMethodInfo = controllerType.GetMethod(actionName)
      Dim actionReturnType = actionMethodInfo.ReturnType.Name

      Select Case actionReturnType
        Case "PartialViewResult"
          filterContext.Result = New RedirectResult("/Home/UnavailablePartial/")
        Case "JsonResult"
          filterContext.Result = New RedirectResult("/Home/UnavailableJson/")
        Case Else
          filterContext.Result = New RedirectResult("/Home/Unavailable/")
      End Select

    End If
  Catch ex As Exception
    _eventLogger.LogWarning(ex, EventLogEntryType.Error)
    Throw
  End Try
End Sub

I can use reflection to find the return type of the action method. My problem is if I have the following methods on a controller:

Public Function Create() As ViewResult
  Return View()
End Function

<AcceptVerbs(HttpVerbs.Post)>
Public Function Create(values as FormCollection) As ViewResult
  ' Do stuff here
End Function

I get an AmbiguousMatchException thrown.

With the information I have in the OnActionExecuting method, is there anyway of being more precise with determining the overload that is being called?

Nick
  • 3,637
  • 9
  • 42
  • 56

3 Answers3

9

I created an AuthenticationFilterAttribute based on this which returns different results based on type:

    /// <summary>
    /// Access to the action will be blocked if the user is not logged in. 
    ///  Apply this to the controller level or individual actions as an attribute.
    /// </summary>
    public class AuthenticationFilterAttribute : ActionFilterAttribute
    {
        protected const string InvalidAccess = "Invalid access";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // Find out if the user is logged in: 
            Controller controller = (Controller)filterContext.Controller;
            if (!controller.User.Identity.IsAuthenticated)
            {
                switch (GetExpectedReturnType(filterContext).Name)
                {
                    case "JsonResult":
                        var jsonResult = new JsonResult();
                        jsonResult.Data = new { Error = true, ErrorMessage = InvalidAccess };
                        jsonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
                        filterContext.Result = jsonResult;
                        break;

                    // Assume same behaviour as ActionResult
                    default: 
                        var actionResult = new ContentResult();
                        actionResult.Content = InvalidAccess;
                        filterContext.Result = actionResult;
                        break;
                }
            }
        }

        private Type GetExpectedReturnType(ActionExecutingContext filterContext)
        {
            // Find out what type is expected to be returned
            string actionName = filterContext.ActionDescriptor.ActionName;
            Type controllerType = filterContext.Controller.GetType();
            MethodInfo actionMethodInfo = default(MethodInfo);
            try
            {
                actionMethodInfo = controllerType.GetMethod(actionName);
            }
            catch (AmbiguousMatchException ex)
            {
                // Try to find a match using the parameters passed through
                var actionParams = filterContext.ActionParameters;
                List<Type> paramTypes = new List<Type>();
                foreach (var p in actionParams)
                {
                    paramTypes.Add(p.Value.GetType());
                }

                actionMethodInfo = controllerType.GetMethod(actionName, paramTypes.ToArray());
            }

            return actionMethodInfo.ReturnType;
        }
    }
basarat
  • 207,493
  • 46
  • 386
  • 462
  • 1
    Interesting solution, thanks. Note that if filterContext.ActionDescriptor is of type System.Web.Mvc.ReflectedActionDescriptor, it will already have the MethodInfo property so you don't need to go to the trouble of determining it. – Tobias J Nov 26 '13 at 16:39
2

Ok, this is the solution I have come up with.

Public Overrides Sub OnActionExecuting(ByVal filterContext As System.Web.Mvc.ActionExecutingContext)
  Try
    ' Check controller name against database.
    Dim controllerName = filterContext.Controller.GetType().Name
    Dim shortName = controllerName.Remove(controllerName.Length - 10)
    ' Look up availability.
    Dim available As Boolean = _coreService.GetControllerAvailability(shortName)
    If Not available Then
      ' find out what type is expected to be returned
      Dim actionName As String = filterContext.ActionDescriptor.ActionName
      Dim controllerType = Type.GetType("Attenda.Stargate.Web." & controllerName)
      Dim actionMethodInfo As MethodInfo
      Try
        actionMethodInfo = controllerType.GetMethod(actionName)
      Catch ex As AmbiguousMatchException
        ' Try to find a match using the parameters passed through
        Dim actionParams = filterContext.ActionParameters
        Dim paramTypes As New List(Of Type)
        For Each p In actionParams
          paramTypes.Add(p.Value.GetType())
        Next
        actionMethodInfo = controllerType.GetMethod(actionName, paramTypes.ToArray)
      End Try
      Dim actionReturnType = actionMethodInfo.ReturnType.Name

      Select Case actionReturnType
        Case "PartialViewResult"
          filterContext.Result = New RedirectResult("/Home/UnavailablePartial/")
        Case "JsonResult"
          filterContext.Result = New RedirectResult("/Home/UnavailableJson/")
        Case Else
          filterContext.Result = New RedirectResult("/Home/Unavailable/")
      End Select

    End If
  Catch ex As Exception
    _eventLogger.LogWarning(ex, EventLogEntryType.Error)
    Throw
  End Try
End Sub

If the Type.GetMethod(string) call fails to identify the method requested, I fetch the parameters collection from the ActionExecutingContext.ActionParameters collection and build an array of the types of the parameters passed in the request. I can then use the Type.GetMethod(string,type()) overload to be more specific about my request.

Nick
  • 3,637
  • 9
  • 42
  • 56
0

By the time OnActionExecuting is invoked, the action method has not been executed yet, so there's no way you know whether that action method is going to return which subclass of ActionResult. So, unless you can go with CIL analysis implementation (which I think can get ugly very quickly), I don't think what you want to do is possible.

That said, isn't the fact that you redirect users to a view when the controller isn't available enough? I mean, I don't understand why you want to redirect users to a JSON result or a partial view.

Buu Nguyen
  • 46,887
  • 5
  • 64
  • 84
  • The site is a portal for our clients. I have some pages such as the homepage with partial views from other controllers. I want to return a partial view with a message in back to the parent view. The home controller will always be available but the reports controller may not be. The reports widget should just show a polite message. – Nick May 19 '10 at 12:39
  • @Nick: then why not just do something like filterContext.Result = New PartialViewResult(...), regardless of the actual action result to be returned by action method? – Buu Nguyen May 19 '10 at 13:28
  • That's ok if they are expecting the partial view. If they hit /Reports/Index though they won't like a bare partialview coming back. I've updated my question with the progress I've made using reflection. – Nick May 19 '10 at 13:52
  • I thought about the reflection approach too but that only works if you strictly declare your action method to return a specific subclass of ActionResult. That might not always be possible, because you might have some conditional code, like if (cond1) return Json(...); else return JavaScript(...); Plus, it's almost a convention to just use ActionResult regardless of the specific subclass returned. That said, if you can live with these known constraints that then reflection is an option. – Buu Nguyen May 19 '10 at 15:19
  • 2
    @Nick: regarding the updated post: to correctly pick out the method, among many different overloads, you need to follow the resolution steps that MVC does to find the match. Specifically, you should look at the method FindActionMethod of the internal class ActionMethodSelector (in MVC 2 source code). If you're lucky, then you might be able to reuse the method without much change & many dependencies - I haven't tried it myself though. – Buu Nguyen May 19 '10 at 15:31
  • Thanks for that, I downloaded the source and had a good poke around. It seems to use a very similar method to the answer I came up with :) – Nick May 24 '10 at 10:54