3

I have a list of URLs that I obtained by querying Google Analytics data. I want to run each of these URLs through the MVC pipeline to get the ActionResult. The action result contains the view model from which I can extract some important information.

Based on the extensibility of MVC, I thought this would be easy. I thought I could mock up a HttpRequest using the string URL and pass it through the routing and controller. My end point would be invoking the action method which would return the ActionResult. I'm finding bits and pieces of what I need, but a lot of the methods are protected within the various classes and the documentation on them is pretty sparse.

I somehow want to reach in to the ControllerActionInvoker and get the result of the call to the protected function InvokeActionMethod.

Ben Mills
  • 27,454
  • 14
  • 42
  • 38
  • How are you *Running* these urls? – Erik Philips Dec 20 '12 at 21:17
  • I just mean that my input is a string URL that I want to pass in to the MVC pipeline and extract the ActionResult after the action method is called. – Ben Mills Dec 20 '12 at 21:22
  • are these URL's in your MVC application. I mean are you trying to run through these url's in the same application. – Sunny Dec 20 '12 at 21:27
  • The URLs are coming from a Google Analytics report. They are real URLs that my MVC website handles. I want to fake a request for one of these URLs and extract the view model from the ActionResult (rather than rendering a view). – Ben Mills Dec 20 '12 at 21:34
  • I think this article would help. http://haacked.com/archive/2007/12/17/testing-routes-in-asp.net-mvc.aspx – jes Dec 20 '12 at 21:59

4 Answers4

3

First of all, Darin's answer got me started, but there's a lot more detail to the final solution, so I'm adding a separate answer. This one is complex, so bear with me.

There are 4 steps to getting the ViewResult from a URL:

  1. Mock the RequestContext via the routing system (Darin's answer got me started on this).

            Uri uri = new Uri(MyStringUrl);
            var request = new HttpRequest(null, uri.Scheme + "://" + uri.Authority + uri.AbsolutePath, string.IsNullOrWhiteSpace(uri.Query) ? null : uri.Query.Substring(1));
            var response = new HttpResponse(new StringWriter());
            var context = new HttpContext(request, response);
            var contextBase = new HttpContextWrapper(context);
            var routeData = System.Web.Routing.RouteTable.Routes.GetRouteData(contextBase);
    
            // We shouldn't have to do this, but the way we are mocking the request doesn't seem to pass the querystring data through to the route data.
            foreach (string key in request.QueryString.Keys)
            {
                if (!routeData.Values.ContainsKey(key))
                {
                    routeData.Values.Add(key, request.QueryString[key]);
                }
            }
    
            var requestContext = new System.Web.Routing.RequestContext(contextBase, routeData);
    
  2. Subclass your controller. Add a public method that allows you to call the protected Execute(RequestContext) method.

    public void MyExecute(System.Web.Routing.RequestContext requestContext)
    {
        this.Execute(requestContext);
    }
    
  3. In the same subclassed controller, Add a public event that hooks in to the protected OnActionExecuted event. This allows you to reach in a grab the ViewResult via the ActionExecutedContext.

    public delegate void MyActionExecutedHandler(ActionExecutedContext filterContext);
    
    public event MyActionExecutedHandler MyActionExecuted;
    
    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);
        if (MyActionExecuted != null)
        {
            MyActionExecuted(filterContext);
        }
    }
    
  4. Tie everything together by instantiating an instance of the new controller subclass, adding an event handler, and calling the new public execute method (passing in the mocked RequestContext). The event handler will give you access to the ViewResult.

            using (MyCompany.Controllers.MyController c = new Controllers.MyController())
            {
                c.MyActionExecuted += GrabActionResult;
                try
                {
                    c.MyExecute(requestContext);
                }
                catch (Exception)
                {
                    // Handle an exception.
                }
            }
    

and here's the event handler:

        private void GrabActionResult(System.Web.Mvc.ActionExecutedContext context)
        {
            if (context.Result.GetType() == typeof(ViewResult))
            {
                ViewResult result = context.Result as ViewResult;
            }
            else if (context.Result.GetType() == typeof(RedirectToRouteResult))
            {
                // Handle.
            }
            else if (context.Result.GetType() == typeof(HttpNotFoundResult))
            {
                // Handle.
            }
            else
            {
                // Handle.
            }
        }
Ben Mills
  • 27,454
  • 14
  • 42
  • 38
1

The difficulty here consists into parsing the url into its constituent controller and action. Here's how this could be done:

var url = "http://example.com/Home/Index";
var request = new HttpRequest(null, url, "");
var response = new HttpResponse(new StringWriter.Null);
var context = new HttpContext(request, response);
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(context));
var values = routeData.Values;
var controller = values["controller"];
var action = values["action"];

Now that you know the controller and the action you could use reflection to instantiate and execute it.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
0

Try this:

object result = null;
Type controller = Type.GetType("MvcApplication4.Controllers.HomeController");
if (controller != null)
{
    object controllerObj = Activator.CreateInstance(controller, null);
    if (controller.GetMethod("ActionName") != null)
    {
        result = ((ViewResult)controller.GetMethod("ActionName").Invoke(controllerObj, null)).ViewData.Model;
    }
}

I assumed normal routes are configured in the application and can be retrieved using regex or string operations. Following your discussion, I learned that you guys want to really follow through the MVC pipeline by digging into the framework by not using reflection or any hardcording techniques. However, I tried to search to minimize hardcoding by trying to match the url with the routes configured in the application by following this thread

How to determine if an arbitrary URL matches a defined route

Also, I came across other thread which creates httprequest to access routedata object but again reflection needs to be used for this.

String URL to RouteValueDictionary

Community
  • 1
  • 1
Sunny
  • 4,765
  • 5
  • 37
  • 72
  • Where is the url in your example? All you seem to be doing is instantiating a controller and calling an action on it. The OP stated in his question that he has an url as input (such as `http://example.com/Home/Index`) and he wanted to obtain the ActionResult that was returned by the corresponding controller action that served this url. – Darin Dimitrov Dec 20 '12 at 21:39
  • Since its MVC application, I thought controller and action names can be extracted from URL and using foreach loop, controller and actionname can be replaced ? – Sunny Dec 20 '12 at 21:41
  • If I can mock the HttpContext, then I can get the RouteData object using System.Web.Routing.RouteTable.Routes.GetRouteData(MockedHttpContext), but that's as far as I've got. This effectively uses all the logic in my routing to get the controller and action name, but I don't know how to use this to call the action method (inluding parsing parameters and applying filters). All of that logic is buried in the ActionInvoker. – Ben Mills Dec 20 '12 at 21:45
  • 1
    @BenMills, forget about mocking HttpContext, you can't do that and all you will end up is wasting your time. – Darin Dimitrov Dec 20 '12 at 21:50
  • @DarinDimitrov I just read an article that basically said the same thing. So there's no way of passing a string URL through the routing system to get a RouteData object? If that's true, then I think my task is impossible. I would have to parse the URLs using a new set of regular expressions and call the action methods directly. That would be a shame as all the smarts is already in my routing. – Ben Mills Dec 20 '12 at 21:55
0

Thanks Ben Mills, this got me started with my own problem. However I found that I didn't have to do 2, 3 or 4, by doing the following.

Uri uri = new Uri(MyStringUrl);
var absoluteUri = uri.Scheme + "://" + uri.Authority + uri.AbsolutePath;
var query = string.IsNullOrWhiteSpace(uri.Query) ? null : uri.Query.Substring(1);
var request = new HttpRequest(null, absoluteUri, query);

Getting access to the string writer is important.

var sw = new StringWriter();
var response = new HttpResponse(sw);
var context = new HttpContext(request, response);
var contextBase = new HttpContextWrapper(context);
var routeData = System.Web.Routing.RouteTable.Routes.GetRouteData(contextBase);

If we assign the RouteData to the request context we can use the MVC pipeline as intended.

request.RequestContext.RouteData = routeData;

var controllerName = routeData.GetRequiredString("controller");

var factory = ControllerBuilder.Current.GetControllerFactory();
var contoller = factory.CreateController(request.RequestContext, controllerName);

controller.Execute(request.RequestContext);

var viewResult = sw.ToString(); // this is our view result.

factory.ReleaseController(controller);
sw.Dispose();

I hope this helps someone else wanting to achieve similar things.

benembery
  • 666
  • 7
  • 20