Run a URL string through the ASP.NET MVC pipeline to get an ActionResult - asp.net-mvc

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.

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:
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);
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);
}
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);
}
}
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.
}
}

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.

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

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.

Related

How test that ASP.NET MVC route redirects to other site?

Due to a prinitng error in some promotional material I have a site that is receiving a lot of requests which should be for one site arriving at another.
i.e.
The valid sites are http://site1.com/abc & http://site2.com/def but people are being told to go to http://site1.com/def.
I have control over site1 but not site2.
site1 contains logic for checking that the first part of the route is valid in an actionfilter, like this:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
if ((!filterContext.ActionParameters.ContainsKey("id"))
|| (!manager.WhiteLabelExists(filterContext.ActionParameters["id"].ToString())))
{
if (filterContext.ActionParameters["id"].ToString().ToLowerInvariant().Equals("def"))
{
filterContext.HttpContext.Response.Redirect("http://site2.com/def", true);
}
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
filterContext.HttpContext.Response.Clear();
}
}
I'm not sure how to test the redirection to the other site though.
I already have tests for redirecting to "NoWhiteLabel" using the MvcContrib Test Helpers, but these aren't able to handle (as far as I can see) this situation.
How do I test the redirection to antoher site?
I would recommend you using RedirectResult instead of calling Response.Redirect:
if (youWantToRedirect)
{
filterContext.Result = new RedirectResult("http://site2.com/def")
}
else
{
filterContext.Result = new ViewResult { ViewName = "NoWhiteLabel" };
}
Now if you know how to test ViewResult with MVCContrib TestHelper you will be able to test the RedirectResult the same way. The tricky part is mocking the manager to force it to satisfy the if condition.
UPDATE:
Here's how a sample test might look like:
// arrange
var mock = new MockRepository();
var controller = mock.StrictMock<Controller>();
new TestControllerBuilder().InitializeController(controller);
var sut = new MyFilter();
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
new Dictionary<string, object>());
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");
Update (By Matt Lacey)
Here's how I actually got this working:
// arrange
var mock = new MockRepository();
// Note that in the next line I create an actual instance of my real controller - couldn't get a mock to work correctly
var controller = new HomeController(new Stubs.BlankContextInfoProvider(), new Stubs.BlankWhiteLabelManager());
new TestControllerBuilder().InitializeController(controller);
var sut = new UseBrandedViewModelAttribute(new Stubs.BlankWhiteLabelManager());
var aec = new ActionExecutingContext(
controller.ControllerContext,
mock.StrictMock<ActionDescriptor>(),
// being sure to specify the necessary action parameters
new Dictionary<string, object> { { "id", "def" } });
// act
sut.OnActionExecuting(aec);
// assert
aec.Result.ShouldBe<RedirectResult>("");
var result = (RedirectResult)aec.Result;
result.Url.ShouldEqual("http://site2.com/def", "");

How do I convert an HttpRequestBase into an HttpRequest object?

inside my ASP.NET MVC controller, I've got a method that requires an HttpRequest object. All I have access to is an HttpRequestBase object.
Is there anyway I can somehow convert this?
What can/should I do??
You should always use HttpRequestBase and HttpResponseBase in your application as opposed to the concrete versions which are impossible to test (without typemock or some other magic).
Simply use the HttpRequestWrapper class to convert as shown below.
var httpRequestBase = new HttpRequestWrapper(Context.Request);
Is it your method, so you can re-write it to take HttpRequestBase? If not, you can always get the current HttpRequest from HttpContext.Current.HttpRequest to pass on. However, I often wrap access to the HttpContext inside a class like mentioned in ASP.NET: Removing System.Web Dependencies for better unit testing support.
You can just use
System.Web.HttpContext.Current.Request
The key here is that you need the full namespace to get to the "correct" HttpContext.
I know it's been 4 years since this question was asked, but if this will help somebody, then here you go!
(Edit: I see that Kevin Hakanson already gave this answer...so hopefully my response will help those people who just read answers and not comments.) :)
To get HttpRequest in ASP.NET MVC4 .NET 4.5, you can do the following:
this.HttpContext.ApplicationInstance.Context.Request
Try to use/create a HttpRequestWrapper using your HttpRequestBase.
Typically when you need to access the HttpContext property in a controller action, there is something you can do better design wise.
For example, if you need to access the current user, give your action method a parameter of type IPrincipal, which you populate with an Attribute and mock as you wish when testing. For a small example on how, see this blog post, and specifically point 7.
There is no way to convert between these types.
We had a similar case. We rewrote our classes/web services methods so that they use HttpContextBase, HttpApplicationStateBase, HttpServerUtilityBase, HttpSessionStateBase... instead of the types of close name without the "Base" suffix (HttpContext, ... HttpSessionState). They are a lot easier to handle with home-made mocking.
I feel sorry you couldn't do it.
This is an ASP.Net MVC 3.0 AsyncController which accepts requests, converts the inbound HttpRequestBase MVC object to a System.Web.HttpWebRequest. It then sends the request asynchronously. When the response comes back, it converts the System.Web.HttpWebResponse back into an MVC HttpResponseBase object which can be returned via the MVC controller.
To answer this question explicitly, I guess you'd only be interested in the BuildWebRequest() function. However, it demonstrates how to move through the whole pipeline - converting from BaseRequest > Request and then Response > BaseResponse. I thought sharing both would be useful.
Through these classes, you can have an MVC server which acts as a web proxy.
Hope this helps!
Controller:
[HandleError]
public class MyProxy : AsyncController
{
[HttpGet]
public void RedirectAsync()
{
AsyncManager.OutstandingOperations.Increment();
var hubBroker = new RequestBroker();
hubBroker.BrokerCompleted += (sender, e) =>
{
this.AsyncManager.Parameters["brokered"] = e.Response;
this.AsyncManager.OutstandingOperations.Decrement();
};
hubBroker.BrokerAsync(this.Request, redirectTo);
}
public ActionResult RedirectCompleted(HttpWebResponse brokered)
{
RequestBroker.BuildControllerResponse(this.Response, brokered);
return new HttpStatusCodeResult(Response.StatusCode);
}
}
This is the proxy class which does the heavy lifting:
namespace MyProxy
{
/// <summary>
/// Asynchronous operation to proxy or "broker" a request via MVC
/// </summary>
internal class RequestBroker
{
/*
* HttpWebRequest is a little protective, and if we do a straight copy of header information we will get ArgumentException for a set of 'restricted'
* headers which either can't be set or need to be set on other interfaces. This is a complete list of restricted headers.
*/
private static readonly string[] RestrictedHeaders = new string[] { "Accept", "Connection", "Content-Length", "Content-Type", "Date", "Expect", "Host", "If-Modified-Since", "Range", "Referer", "Transfer-Encoding", "User-Agent", "Proxy-Connection" };
internal class BrokerEventArgs : EventArgs
{
public DateTime StartTime { get; set; }
public HttpWebResponse Response { get; set; }
}
public delegate void BrokerEventHandler(object sender, BrokerEventArgs e);
public event BrokerEventHandler BrokerCompleted;
public void BrokerAsync(HttpRequestBase requestToBroker, string redirectToUrl)
{
var httpRequest = BuildWebRequest(requestToBroker, redirectToUrl);
var brokerTask = new Task(() => this.DoBroker(httpRequest));
brokerTask.Start();
}
private void DoBroker(HttpWebRequest requestToBroker)
{
var startTime = DateTime.UtcNow;
HttpWebResponse response;
try
{
response = requestToBroker.GetResponse() as HttpWebResponse;
}
catch (WebException e)
{
Trace.TraceError("Broker Fail: " + e.ToString());
response = e.Response as HttpWebResponse;
}
var args = new BrokerEventArgs()
{
StartTime = startTime,
Response = response,
};
this.BrokerCompleted(this, args);
}
public static void BuildControllerResponse(HttpResponseBase httpResponseBase, HttpWebResponse brokeredResponse)
{
if (brokeredResponse == null)
{
PerfCounters.ErrorCounter.Increment();
throw new GriddleException("Failed to broker a response. Refer to logs for details.");
}
httpResponseBase.Charset = brokeredResponse.CharacterSet;
httpResponseBase.ContentType = brokeredResponse.ContentType;
foreach (Cookie cookie in brokeredResponse.Cookies)
{
httpResponseBase.Cookies.Add(CookieToHttpCookie(cookie));
}
foreach (var header in brokeredResponse.Headers.AllKeys
.Where(k => !k.Equals("Transfer-Encoding", StringComparison.InvariantCultureIgnoreCase)))
{
httpResponseBase.Headers.Add(header, brokeredResponse.Headers[header]);
}
httpResponseBase.StatusCode = (int)brokeredResponse.StatusCode;
httpResponseBase.StatusDescription = brokeredResponse.StatusDescription;
BridgeAndCloseStreams(brokeredResponse.GetResponseStream(), httpResponseBase.OutputStream);
}
private static HttpWebRequest BuildWebRequest(HttpRequestBase requestToBroker, string redirectToUrl)
{
var httpRequest = (HttpWebRequest)WebRequest.Create(redirectToUrl);
if (requestToBroker.Headers != null)
{
foreach (var header in requestToBroker.Headers.AllKeys)
{
if (RestrictedHeaders.Any(h => header.Equals(h, StringComparison.InvariantCultureIgnoreCase)))
{
continue;
}
httpRequest.Headers.Add(header, requestToBroker.Headers[header]);
}
}
httpRequest.Accept = string.Join(",", requestToBroker.AcceptTypes);
httpRequest.ContentType = requestToBroker.ContentType;
httpRequest.Method = requestToBroker.HttpMethod;
if (requestToBroker.UrlReferrer != null)
{
httpRequest.Referer = requestToBroker.UrlReferrer.AbsoluteUri;
}
httpRequest.UserAgent = requestToBroker.UserAgent;
/* This is a performance change which I like.
* If this is not explicitly set to null, the CLR will do a registry hit for each request to use the default proxy.
*/
httpRequest.Proxy = null;
if (requestToBroker.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase))
{
BridgeAndCloseStreams(requestToBroker.InputStream, httpRequest.GetRequestStream());
}
return httpRequest;
}
/// <summary>
/// Convert System.Net.Cookie into System.Web.HttpCookie
/// </summary>
private static HttpCookie CookieToHttpCookie(Cookie cookie)
{
HttpCookie httpCookie = new HttpCookie(cookie.Name);
foreach (string value in cookie.Value.Split('&'))
{
string[] val = value.Split('=');
httpCookie.Values.Add(val[0], val[1]);
}
httpCookie.Domain = cookie.Domain;
httpCookie.Expires = cookie.Expires;
httpCookie.HttpOnly = cookie.HttpOnly;
httpCookie.Path = cookie.Path;
httpCookie.Secure = cookie.Secure;
return httpCookie;
}
/// <summary>
/// Reads from stream into the to stream
/// </summary>
private static void BridgeAndCloseStreams(Stream from, Stream to)
{
try
{
int read;
do
{
read = from.ReadByte();
if (read != -1)
{
to.WriteByte((byte)read);
}
}
while (read != -1);
}
finally
{
from.Close();
to.Close();
}
}
}
}
It worked like Kevin said.
I'm using a static method to retrieve the HttpContext.Current.Request, and so always have a HttpRequest object for use when needed.
Here in Class Helper
public static HttpRequest GetRequest()
{
return HttpContext.Current.Request;
}
Here in Controller
if (AcessoModel.UsuarioLogado(Helper.GetRequest()))
Here in View
bool bUserLogado = ProjectNamespace.Models.AcessoModel.UsuarioLogado(
ProjectNamespace.Models.Helper.GetRequest()
);
if (bUserLogado == false) { Response.Redirect("/"); }
My Method UsuarioLogado
public static bool UsuarioLogado(HttpRequest Request)

String URL to RouteValueDictionary

Is there as easy way to convert string URL to RouteValueDictionary collection? Some method like UrlToRouteValueDictionary(string url).
I need such method because I want to 'parse' URL according to my routes settings, modify some route values and using urlHelper.RouteUrl() generate string URL according to modified RouteValueDictionary collection.
Thanks.
Here is a solution that doesn't require mocking:
var request = new HttpRequest(null, "http://localhost:3333/Home/About", "testvalue=1");
var response = new HttpResponse(new StringWriter());
var httpContext = new HttpContext(request, response);
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
var values = routeData.Values;
// The following should be true for initial version of mvc app.
values["controller"] == "Home"
values["action"] == "Index"
Hope this helps.
You would need to create a mocked HttpContext as routes constrains requires it.
Here is an example that I use to unit test my routes (it was copied from Pro ASP.Net MVC framework):
RouteCollection routeConfig = new RouteCollection();
MvcApplication.RegisterRoutes(routeConfig);
var mockHttpContext = new MockedHttpContext(url);
RouteData routeData = routeConfig.GetRouteData(mockHttpContext.Object);
// routeData.Values is an instance of RouteValueDictionary
//...
I wouldn't rely on RouteTable.Routes.GetRouteData from previous examples because in that case you risk missing some values (for example if your query string parameters don't fully fit any of registered mapping routes). Also, my version doesn't require mocking a bunch of request/response/context objects.
public static RouteValueDictionary UrlToRouteValueDictionary(string url)
{
int queryPos = url.IndexOf('?');
if (queryPos != -1)
{
string queryString = url.Substring(queryPos + 1);
var valuesCollection = HttpUtility.ParseQueryString(queryString);
return new RouteValueDictionary(valuesCollection.AllKeys.ToDictionary(k => k, k => (object)valuesCollection[k]));
}
return new RouteValueDictionary();
}
Here is a solution that doesn't require instantiating a bunch of new classes.
var httpContext = context.Get<System.Web.HttpContextWrapper>("System.Web.HttpContextBase");
var routeData = System.Web.Routing.RouteTable.Routes.GetRouteData(httpContext);
var values = routeData.Values;
var controller = values["controller"];
var action = values["action"];
The owin context contains an environment that includes the HttpContext.

Url Form Action Without ViewContext

Is it possible to get a URL from an action without knowing ViewContext (e.g., in a controller)? Something like this:
LinkBuilder.BuildUrlFromExpression(ViewContext context, Expression<Action<T>> action)
...but using Controller.RouteData instead of ViewContext. I seem to have metal block on this.
Here's how I do it in a unit test:
private string RouteValueDictionaryToUrl(RouteValueDictionary rvd)
{
var context = MvcMockHelpers.FakeHttpContext("~/");
// _routes is a RouteCollection
var vpd = _routes.GetVirtualPath(
new RequestContext(context, _
routes.GetRouteData(context)), rvd);
return vpd.VirtualPath;
}
Per comments, I'll adapt to a controller:
string path = RouteTable.Routes.GetVirtualPath(
new RequestContext(HttpContext,
RouteTable.Routes.GetRouteData(HttpContext)),
new RouteValueDictionary(
new { controller = "Foo",
action = "Bar" })).VirtualPath;
Replace "Foo" and "Bar" with real names. This is off the top of my head, so I can't guarantee that it's the most efficient solution possible, but it should get you on the right track.
Craig, Thanks for the correct answer. It works great, and it also go me thinking. So in my drive to eliminate those refactor-resistent "magic strings" I have developed a variation on your solution:
public static string GetUrlFor<T>(this HttpContextBase c, Expression<Func<T, object>> action)
where T : Controller
{
return RouteTable.Routes.GetVirtualPath(
new RequestContext(c, RouteTable.Routes.GetRouteData(c)),
GetRouteValuesFor(action)).VirtualPath;
}
public static RouteValueDictionary GetRouteValuesFor<T>(Expression<Func<T, object>> action)
where T : Controller
{
var methodCallExpresion = ((MethodCallExpression) action.Body);
var controllerTypeName = methodCallExpresion.Object.Type.Name;
var routeValues = new RouteValueDictionary(new
{
controller = controllerTypeName.Remove(controllerTypeName.LastIndexOf("Controller")),
action = methodCallExpresion.Method.Name
});
var methodParameters = methodCallExpresion.Method.GetParameters();
for (var i = 0; i < methodParameters.Length; i++)
{
var value = Expression.Lambda(methodCallExpresion.Arguments[i]).Compile().DynamicInvoke();
var name = methodParameters[i].Name;
routeValues.Add(name, value);
}
return routeValues;
}
I know what some will say...dreaded reflection! In my particular application, I think the benefit of maintainability outweighs performance conerns. I welcome any feedback on this idea and the code.

Unit Testing: Creating a 'mock' request to simulate a MVC page request

How do I go about creating a mock request for my asp.net-mvc application for unit-testing?
What options do I have?
I am using FormsCollection in my Actions so I can simulate form input data also.
You just have to create a new instance of FormCollection and add the data inside of it.
So you can call something like this without mocking anything.
var result = controller.Create(new FormCollection { { "InvoiceId", "-1" } }) as RedirectToRouteResult;
Otherwise if your code calls something like Request or HttpContext you can use the following extension method (inspired from Scott Hanselman's example)
I am using RhinoMocks.
public static HttpContextBase SetHttpContext(this MockRepository mocks, Controller controller, HttpCookieCollection cookies) {
cookies = cookies ?? new HttpCookieCollection();
var request = mocks.StrictMock<HttpRequestBase>();
var context = mocks.StrictMock<HttpContextBase>();
var response = mocks.StrictMock<HttpResponseBase>();
SetupResult.For(context.Request).Return(request);
SetupResult.For(context.Response).Return(response);
SetupResult.For(request.Cookies).Return(cookies);
SetupResult.For(request.IsSecureConnection).Return(false);
SetupResult.For(response.Cookies).Return(cookies);
if (controller != null)
{
controller.ControllerContext = new ControllerContext(context, new RouteData(), controller);
}
if (!string.IsNullOrEmpty(requestUrl))
{
request.SetupRequestUrl(requestUrl);
SetupResult.For(response.ApplyAppPathModifier(null)).IgnoreArguments().Return(null);
}
return context;
}

Resources