So i've created my own ControllerFactory and i'm overloading GetControllerSessionBehavior in order to extend the MVC behavior.
To do my custom work i have to use reflection on the called action. However i've stumbled upon a weird issue - i can't retrieve the action by accessing RequestContext.RouteData
While setting up a reproduction sample for this i was not able to reproduce the error.
Is anyone aware of possible reasons for this or knows how to retrieve the action by calling a method with the request context other than this?
public class CustomControllerFactory : DefaultControllerFactory
{
protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
{
if (!requestContext.RouteData.Values.ContainsKey("action"))
return base.GetControllerSessionBehavior(requestContext, controllerType);
var controllerAction = requestContext.RouteData.Values["action"];
var action = controllerAction.ToString();
var actionMethod = controllerType.GetMember(action, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public).FirstOrDefault();
if(actionMethod == null)
return base.GetControllerSessionBehavior(requestContext, controllerType);
var cattr = actionMethod.GetCustomAttribute<SessionStateActionAttribute>();
if (cattr != null)
return cattr.Behavior;
return base.GetControllerSessionBehavior(requestContext, controllerType);
}
}
Action which i can call just fine but can't access the action name of within my controller factory:
[Route("Open/{createModel:bool?}/{tlpId:int}/{siteId:int?}")]
public ActionResult Open(int tlpId, int? siteId, bool? createModel = true)
{
}
Any ideas welcome.
Update:
The problem seems to be related to attribute routing. While it's working fine in repro it doesn't work in production for me.
Found this along the way - Once this is answered i'll have my proper solution too i guess.
Update 2:
Interesting. Reproduction MVC Version 5.0.0.0, Production 5.2.2. Possible introduction of bug?
I can confirm that there was a breaking change to attribute routing between 5.0.0 and 5.1.1. I reported the issue here. However, for my use case Microsoft was able to provide an acceptable workaround.
On the other hand, the problem you are bumping into looks like another culprit. For attribute routing, the route values are stored in a nested route key named MS_DirectRouteMatches. I am not sure exactly which version that changed in, but I know it happened v5+.
So, to fix your issue, you will need to check for the existence of a nested RouteData collection, and use instead of the normal RouteData in the case it exists.
var routeData = requestContext.RouteData;
if (routeData.Values.ContainsKey("MS_DirectRouteMatches"))
{
routeData = ((IEnumerable<RouteData>)routeData.Values["MS_DirectRouteMatches"]).First();
}
var controllerAction = routeData.Values["action"];
var action = controllerAction.ToString();
BTW - In the linked question you provided, the asker assumed that there is a possibility where a request can match more than one route. But that is not possible - a request will match 0 or 1 route, but never more than one.
Related
Following is the code snippet for which I want to write unit tests:
[HttpGet]
public ActionResult Edit(string id)
{
if (Request.IsAjaxRequest())
{
EditModel model = new EditModel();
.....
}
return View();
}
I want to write unit tests for this action where I can fake the result of Request.IsAjaxRequest() to true so that I can write tests for rest of the code of the action.
I have tried following but it doesn't work. _request.Headers is always empty, and Request.IsAjaxRequest() is always returning false:
[Fact]
public void Get_Edit_AjaxRequest_ExpectedActionCalled()
{
HttpRequestBase _request = A.Fake<HttpRequestBase>();
_request.Headers.Add("X-Requested-With", "XMLHttpRequest");
_controller.ControllerContext = A.Fake<ControllerContext>();
_controller.ControllerContext.HttpContext = _request;
A.CallTo(() => _controller.Request).Returns(_request);
var result = _controller.Edit(1) as RedirectToRouteResult;
}
I always get Request.IsAjaxRequest() as false. Any help on this much appreciated. Thanks
I managed to muddle past the compilation errors and use some information from Chapter 10 of FakeItEasy Succinctly, which is all about ASP.NET MVC.
Generally speaking, the ASP.NET MVC classes are not designed in a way to make them easily fakeable, but I have a test setup that causes IsAjaxRequest to return true. The two main hurdles were getting the controller to use the request object and to make sure that the request object was returning the headers we wanted.
The first part was not hard, but the second required us to have the request object use a concrete NameValueCollection. The faked one that it had been providing by default was not useful, because the right properties weren't virtual. Fortunately, using a real NameValueCollection did the trick.
Try this:
[Fact]
public void Get_Edit_AjaxRequest_ExpectedActionCalled_Blair()
{
HttpRequestBase _request = A.Fake<HttpRequestBase>();
// NameValueCollection is effectively unfakeable due to non-virtual properties,
// but a real one works just fine, so make sure the headers use one of those.
A.CallTo(() => _request.Headers).Returns(new NameValueCollection());
_request.Headers["X-Requested-With"] = "XMLHttpRequest";
var httpContext = A.Fake<HttpContextBase>();
A.CallTo(() => httpContext.Request).Returns(_request);
_controller.ControllerContext = new ControllerContext(
new RequestContext(httpContext, new RouteData()),
_controller);
var result = _controller.Edit(1) as RedirectToRouteResult;
}
Be warned that there will be lots of pitfalls like this in the MVC framework, and continuing to fake them may continue to be frustrating. You may find a more sustainable approach is to extract as much of your logic as is feasible out into plain old testable business classes that don't rely on the MVC framework.
I am completely baffled by this.
I have a public method on my controller which works on my development machine. But when I deploy the app I get an error message saying the method is not found;
[HttpGet]
[Authorize(Roles = "Administrator, AdminIT, ManagerIT")]
public ActionResult ListExistingIT(GridSortOptions sort, int? page)
{
if (Request.QueryString["lastPersonMessage"] == null)
ViewData["LastPersonMessage"] = string.Empty;
else
ViewData["LastPersonMessage"] = Request.QueryString["lastPersonMessage"];
EmployeeListViewModel elvm = new EmployeeListViewModel();
elvm.EmployeeList = EmployeeExtended.GetITEmployees();
if (sort.Column != null)
{
elvm.EmployeeList = elvm.EmployeeList.OrderBy(sort.Column, sort.Direction);
}
elvm.EmployeeList = elvm.EmployeeList.AsPagination(page ?? 1, Utility.GetPageLength());
ViewData["sort"] = sort;
return View(elvm);
}
The error message is; System.Web.HttpException: A public action method 'ListExistingIT' was not found on controller 'SHP.Controllers.EmployeeController'.
Now you might think that IIS is not picking up the latest deployment. However I make a change elsewhere and deploy it, and that works. I also restart IIS as well.
I cannot imagine how this happens, or how to detect where the error could be.
There is plenty of discussion on a similar (the same?) issue here on SO:Intermittent asp.net mvc exception: “A public action method ABC could not be found on controller XYZ.”
I can think of 3 different possibilities:
A conflict with the HttpVerb
causing the method not to be found.
A conflict with the filters applied
causing the method to be avoided.
A routing issue, but this one is
probably the last possibility. You
may want to try testing with the
RouteDebugger and see what that
shows you.
I'm in a bit of a pickle here.
I have an action for which the output is fairly static, until another action is used to update the datasource for the first action. I use HttpResponse.RemoveOutputCacheItem to remove that action's cached output so that it is refreshed next time the user loads it.
Basically I have an action like this:
[OutputCache(Duration=86400, Location=OutputCacheLocation.Server)]
public ActionResult Index()
{
return ...
}
on my HomeController, and another action on another controller that updates the information used in the former:
public ActionResult SaveMenu(int id, Menu menu)
{
...
HttpResponse.RemoveOutputCacheItem(Url.Action("Index", "Home"));
...
}
The crazy thing is that this works, as long as you're either loading the URLs http://site/ or http://site/Home/Index. When you use the URL http://site it never refreshes.
Why is that?
It has to do with the way the OutputCacheAttribute works, specifically on its dependency on RouteData not being null. The relevant part is:
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (!filterContext.IsChildAction)
{
new OutputCachedPage(this._cacheSettings).ProcessRequest(HttpContext.Current);
}
}
The ResultExecutingContext filterContext derives from ControllerContext. This is the source for ControllerContext.IsChildAction:
public virtual bool IsChildAction
{
get
{
RouteData routeData = this.RouteData;
if (routeData == null)
{
return false;
}
return routeData.DataTokens.ContainsKey("ParentActionViewContext");
}
}
So, why is this relevant to your question?
Because when you omit the "/" then your Route does not match anything. The default route is "/". An article that explains this more in depth is here: http://www.58bits.com/blog/2008/09/29/ASPNet-MVC-And-Routing-Defaultaspx.aspx . It was written to explain why the Default.aspx file was necessary in ASP.NET MVC 1 projects, but the reason is rooted in the same place.
So, basically, the RouteData is null, so the OutputCacheAttribute can't work. You can solve your problem by doing what Michael Jasper suggested and leveraging URL Rewriting.
IIS has a very usefull module called URL Rewrite. One of the options is to remove or append a trailing slash to all/specific urls. If it is simply the trailing slash that is the problem, this should work.
I've seen a similar behavior in the way SharePoint behaves. SharePoint became confused with http://site; it was unable to determine if the URL was to a File or a SharePoint Site. There's probably something similar going on here.
You've probably resolved the problem by appending the URL with a trailing slash; but, just in case you haven't:
url = string.Format( "{0}/", url.TrimEnd( '/' ) );
I have a controller that implements a simple Add operation on an entity and redirects to the Details page:
[HttpPost]
public ActionResult Add(Thing thing)
{
// ... do validation, db stuff ...
return this.RedirectToAction<c => c.Details(thing.Id));
}
This works great (using the RedirectToAction from the MvcContrib assembly).
When I'm unit testing this method I want to access the ViewData that is returned from the Details action (so I can get the newly inserted thing's primary key and prove it is now in the database).
The test has:
var result = controller.Add(thing);
But result here is of type: System.Web.Mvc.RedirectToRouteResult (which is a System.Web.Mvc.ActionResult). It doesn't hasn't yet executed the Details method.
I've tried calling ExecuteResult on the returned object passing in a mocked up ControllerContext but the framework wasn't happy with the lack of detail in the mocked object.
I could try filling in the details, etc, etc but then my test code is way longer than the code I'm testing and I feel I need unit tests for the unit tests!
Am I missing something in the testing philosophy? How do I test this action when I can't get at its returned state?
I am using MVC2 RC2 at the moment and the answer from rmacfie didn't quite work for me but did get me on the right track.
Rightly or wrongly I managed to do this in my test instead:
var actionResult = (RedirectToRouteResult)logonController.ForgotUsername(model);
actionResult.RouteValues["action"].should_be_equal_to("Index");
actionResult.RouteValues["controller"].should_be_equal_to("Logon");
Not sure if this will help someone but might save you 10 minutes.
There is MVC Contrib TestHelper that are fantastic for testing most of the ActionResult
You can get it here:
http://mvccontrib.codeplex.com/wikipage?title=TestHelper
Here is an example of the syntax:
var controller = new TestController();
controller.Add(thing)
.AssertActionRedirect()
.ToAction<TestController>(x => x.Index());
To test if the data has been persisted successfully, you should maybe ask your database directly, I don't know if you're using an ORM or something, but you should do something to get the last insterted item in your database, then compare with the value you provided to your Add ActionResult and see if this is ok.
I don't think that testing your Details ActionResult to see if your data is persisted is the right approach. That would not be an unit test, more a functional test.
But you should also unit test your Details method to make sure that your viewdata object is filled with the right data coming from your database.
You seem to be doing way too much for a unit test. The validation and data access would typically be done by services that you call from the controller action. You mock those services and only test that they were called properly.
Something like this (using approximate syntax for Rhino.Mocks & NUnit):
[Test]
public void Add_SavesThingToDB()
{
var dbMock = MockRepository.GenerateMock<DBService>();
dbMock.Expect(x => x.Save(thing)).Repeat.Once();
var controller = new MyController(dbMock);
controller.Add(new Thing());
dbMock.VerifyAllExpectations();
}
[Test]
public void Add_RedirectsAfterSave()
{
var dbMock = MockRepository.GenerateMock<DBService>();
var controller = new MyController(dbMock);
var result = (RedirectToRouteResult)controller.Add(new Thing());
Assert.That(result.Url, Is.EqualTo("/mynew/url"));
}
I have a static helper method that tests redirection.
public static class UnitTestHelpers
{
public static void ShouldEqual<T>(this T actualValue, T expectedValue)
{
Assert.AreEqual(expectedValue, actualValue);
}
public static void ShouldBeRedirectionTo(this ActionResult actionResult, object expectedRouteValues)
{
RouteValueDictionary actualValues = ((RedirectToRouteResult)actionResult).RouteValues;
var expectedValues = new RouteValueDictionary(expectedRouteValues);
foreach (string key in expectedValues.Keys)
{
Assert.AreEqual(expectedValues[key], actualValues[key]);
}
}
}
Then creating a redirection test is very easy.
[Test]
public void ResirectionTest()
{
var result = controller.Action();
result.ShouldBeRedirectionTo(
new
{
controller = "ControllerName",
action = "Index"
}
);
}
Using ASP.NET MVC Preview 5 (though this has also been tried with the Beta), it appears that querystring defaults in a route override the value that is passed in on the query string. A repro is to write a controller like this:
public class TestController : Controller
{
public ActionResult Foo(int x)
{
Trace.WriteLine(x);
Trace.WriteLine(this.HttpContext.Request.QueryString["x"]);
return new EmptyResult();
}
}
With route mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo", x = 1 });
And then invoke it with this relative URI:
/Test/Foo?x=5
The trace output I see is:
1
5
So in other words the default value that was set up for the route is always passed into the method, irrespective of whether it was actually supplied on the querystring. Note that if the default for the querystring is removed, i.e. the route is mapped as follows:
routes.MapRoute(
"test",
"Test/Foo",
new { controller = "Test", action = "Foo" });
Then the controller behaves as expected and the value is passed in as the parameter value, giving the trace output:
5
5
This looks to me like a bug, but I would find it very surprising that a bug like this could still be in the beta release of the ASP.NET MVC framework, as querystrings with defaults aren't exactly an esoteric or edge-case feature, so it's almost certainly my fault. Any ideas what I'm doing wrong?
The best way to look at ASP.NET MVC with QueryStrings is to think of them as values that the route does not know about. As you found out, the QueryString is not part of the RouteData, therefore, you should keep what you are passing as a query string separate from the route values.
A way to work around them is to create default values yourself in the action if the values passed from the QueryString are null.
In your example, the route knows about x, therefore your url should really look like this:
/Test/Foo or /Test/Foo/5
and the route should look like this:
routes.MapRoute("test", "Test/Foo/{x}", new {controller = "Test", action = "Foo", x = 1});
To get the behavior you were looking for.
If you want to pass a QueryString value, say like a page number then you would do this:
/Test/Foo/5?page=1
And your action should change like this:
public ActionResult Foo(int x, int? page)
{
Trace.WriteLine(x);
Trace.WriteLine(page.HasValue ? page.Value : 1);
return new EmptyResult();
}
Now the test:
Url: /Test/Foo
Trace:
1
1
Url: /Test/Foo/5
Trace:
5
1
Url: /Test/Foo/5?page=2
Trace:
5
2
Url: /Test/Foo?page=2
Trace:
1
2
Hope this helps clarify some things.
One of my colleagues found a link which indicates that this is by design and it appears the author of that article raised an issue with the MVC team saying this was a change from earlier releases. The response from them was below (for "page" you can read "x" to have it relate to the question above):
This is by design. Routing does not
concern itself with query string
values; it concerns itself only with
values from RouteData. You should
instead remove the entry for "page"
from the Defaults dictionary, and in
either the action method itself or in
a filter set the default value for
"page" if it has not already been set.
We hope to in the future have an
easier way to mark a parameter as
explicitly coming from RouteData, the
query string, or a form. Until that is
implemented the above solution should
work. Please let us know if it
doesn't!
So it appears that this behaviour is 'correct', however it is so orthogonal to the principle of least astonishment that I still can't quite believe it.
Edit #1: Note that the post details a method of how to provide default values, however this no longer works as the ActionMethod property he uses to access the MethodInfo has been removed in the latest version of ASP.NET MVC. I'm currently working on an alternative and will post it when done.
Edit #2: I've updated the idea in the linked post to work with the Preview 5 release of ASP.NET MVC and I believe it should also work with the Beta release though I can't guarantee it as we haven't moved to that release yet. It's so simple that I've just posted it inline here.
First there's the default attribute (we can't use the existing .NET DefaultValueAttribute as it needs to inherit from CustomModelBinderAttribute):
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class DefaultAttribute : CustomModelBinderAttribute
{
private readonly object value;
public DefaultAttribute(object value)
{
this.value = value;
}
public DefaultAttribute(string value, Type conversionType)
{
this.value = Convert.ChangeType(value, conversionType);
}
public override IModelBinder GetBinder()
{
return new DefaultValueModelBinder(this.value);
}
}
The the custom binder:
public sealed class DefaultValueModelBinder : IModelBinder
{
private readonly object value;
public DefaultValueModelBinder(object value)
{
this.value = value;
}
public ModelBinderResult BindModel(ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;
var queryValue = request .QueryString[bindingContext.ModelName];
return string.IsNullOrEmpty(queryValue)
? new ModelBinderResult(this.value)
: new DefaultModelBinder().BindModel(bindingContext);
}
}
And then you can simply apply it to the method parameters that come in on the querystring, e.g.
public ActionResult Foo([Default(1)] int x)
{
// implementation
}
Works like a charm!
I think the reason querystring parameters do not override the defaults is to stop people hacking the url.
Someone could use a url whose querystring included controller, action or other defaults you didn't want them to change.
I've dealt with this problem by doing what #Dale-Ragan suggested and dealing with it in the action method. Works for me.
I thought the point with Routing in MVC is to get rid of querystrings. Like this:
routes.MapRoute(
"test",
"Test/Foo/{x}",
new { controller = "Test", action = "Foo", x = 1 });