Conditional ASP.NET MVC 3 Routing (With Areas) - asp.net-mvc-3

Hurro.
I'm trying to achieve some conditional routing based on whether the current user is an admin or not. The system only has two modes, admin or non-admin and nothing more than this. I'm using areas for my admin area because the controller names would be the same, but they'll deliver different functionality pretty much in every case.
In this system, however, the admins shouldn't really be aware of their admin location, they just know that they use the system to do something else other than what regular users do. I don't want there to be any distinction between the two in terms of URL because of this. What I want to do is be able to do something like mysite.com/AuditHistory and dependant on whether you're an admin or user will depend on what controller is used. So if it's a user making this request, then it'd use the AuditHistoryController in the regular controllers folder, but if it's an admin then it'd use the AuditHistoryController in Areas/Admin/Controllers.
I've seen the use of IRouteConstraint and can do something along the following lines:
public class AdminRouteConstraint : IRouteConstraint
{
public AdminRouteConstraint() { }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return httpContext.User.IsInRole("Admin");
}
}
With the following:
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", controller = "Home", id = UrlParameter.Optional },
new { controller = new AdminRouteConstraint() }
);
Can I simply get rid of "Admin/" at the front and do the same thing for the other routes but say UserRouteConstraint? I've not seen this done anywhere though and not sure if it's correct.
Any ideas on how to do this?

Could you simply redirect the user from the ActionResult if they are in a role? That is if you don't mind the URL changing?
Something like this...
[Authorize]
public ActionResult AuditHistory()
{
if(Context.User.IsInRole("Admin")
{
return Redirect("Admin/AuditHistory");
}
else
{
return View();
}
}
To me, this is a bit of a hack. But it may be a solution.
Obviously, you would need to do basic checks like making sure the current request is authenticated etc.
If you really don't want the URL to change, you could possibly have two separate views and do away with the admin Area
[Authorize]
public ActionResult AuditHistory()
{
if(Context.User.IsInRole("Admin")
{
return View("AdminAuditHistory", new AdminAuditHistoryViewModel());
}
else
{
return View("AuditHistory", new AuditHistoryViewModel());
}
}
In fact I think this is probably the cleanest solution, but is possibly still a bit of a hack.
I hope this helps.

Related

VS Express MVC3 newbie redirect issue

I'm brand new to MVC (having done classic ASP for many years). I'm not sure I know how to ask this question. Basically, I want the actions of one controller to seamlessly transfer/redirect to another view/controller. I have tried
public class SetupController : Controller
{
...
public ActionResult Bicycles()
{
return RedirectToAction("Index", "Bicycles");
}
}
but the problem is that this takes me to localhost/Bicycles (which doesn't exist). What I want is to go to localhost/Setup/Bicycles. I tried this (adding "Setup" parent folder to controller name):
public class SetupController : Controller
{
...
public ActionResult Bicycles()
{
return RedirectToAction("Index", "Setup/Bicycles");
}
}
but this created an infinite redirect loop, which the browser rightly refused to do.
Hope it makes sense what I'm trying to do.
I believe what you are looking for is:
public ActionResult Bicycles()
{
return RedirectToAction("Bicycles", "Setup");
}
The first parameter is the Action, the second the Controller.
Since you already are in SetupController in Bicycles action, you would get an infinite redirect. However, from what you mentioned, that is where you are attempting to redirected to.
protected internal RedirectToRouteResult RedirectToAction(
string actionName,
string controllerName
)
So your first example redirects to Index action in Bicycles controller, hence the localhost/Bicycles.

MVC [Autorize] plus Roles from string

Hey I got some idea but problem is i can't make this work.
in MVC we can use [Authorize] to "protect" some actions/controllers, we can make next step and give some persmission for a Roles and Users.
[Authorize(Roles="Boss", User="Secretary"]
This working good but its kind of bad becaue in real life we dont know who will have rights for this. So idea was make strings of Roles and Users and back to authorize to make Microsoft magic on this.
[Authoize(Role=RoleString(), User=UserString())]
Ofcourse, its not working, how make this work?
The problem is that AuthorizeAttribute expects a constant for both the User and the Role strings. You will need to make a CustomAuthorizeAttribute that is something like what is found in this blog post.
So lets say you have a string that you store in your web.config that is something like this:
<add key="authorizedUsers" value="Dave,Chuck,Sally" />
and then you have your custom authorize attribute that would be something like this:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public IAuthorizationService _authorizationService { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
var users = System.Configuration.ConfigurationManager.AppSettings["authorizedUsers"].Split(',');
if users.Contains(user.Identity.Name)
{
return true;
}
return _authorizationService.Authorize(httpContext);
}
}
Note that I threw this together rather quickly so it is not tested. You can easily modify this to get user or group names from a database so that it can be fully dynamic.

Recommended API design with ASP.NET MVC3

I'm working with ASP.NET MVC 3. I'm kind of new to it. I think I'm starting to get the hang of it. But there is something that I'm trying to do, that I think makes sense, but maybe I'm wrong.
I'm trying to create an API around Order objects in my database. In order to get all of the orders in the system, I was going to expose an API that looks like the following:
/orders/
In cases where I wanted to get a specific Order, I would simply append an ID. In other words, the URL would look like this:
/orders/12345
In an effort to accomplish this, I created the following controller:
public class OrdersController : Controller
{
// GET: /Orders/
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index()
{
string result = "list of orders";
return Json(result, JsonRequestBehavior.AllowGet);
}
//
// GET: /Orders/{orderID}
public ActionResult Index(int id)
{
string result = "order:" + id;
return Json(result, JsonRequestBehavior.AllowGet);
}
}
In my AreaRegistration class, I have the following:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"OrderList",
"{controller}/{action}",
new { action = "Index", controller="Orders" }
);
context.MapRoute(
"Order",
"{controller}/{action}/{id}",
new { action = "Index", controller = "Orders" }
);
}
When I attempted to access "/orders/", via the browser address bar, I get the JSON like I would expect. However, if I attempt to access "/orders/12345", I receive a 404. What am I missing?
Thank you
You need to also define proper routes in global.asax or use the default route which looks like {controller}/{action}/{id} where controller is defaulted to "Home", action is defaulted to "Index" and id is optional.
So /orders works because you have defined controller (orders), default action (Index) and missing id (which doesn't matter as it is optional)
But when you try /orders/12345 then you have defined controller (orders), action (12345) and missing id
So to make this work with only the default route the request should be /orders/index/12345
edit: for registering area routes you should use AreaRegistration class

ASP.NET MVC3 Nested Resources within an Area

I come from a Rails background and I'm having problems wrapping my head around Microsoft's MVC framework.
Today it's Routing. Rails gives you namespaces (e.g. Admin) which is the equivalent of Areas in .NET MVC3. Rails also allows you to define nested resources within your routes that will give you for example /posts/1/comments/1/edit and in your action you basically get params[:post_id] and params[:id].
I need something similar in ASP.NET MVC3 but not sure how to go about this. Googling for this results in at least 30 different ways to accomplish this and non of them mention areas.
It feels like I should add/modify something within here:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
But not sure where. Any suggestions?
I think you're in the right file (your AreaRegistration.cs file). I prefer being a little more explicit with my routes rather than using the default 'catch all' type of route that they provide. So here's an example of how I'd handle this:
Add something like this before the existing route (or get rid of the existing one all together) in the RegisterArea method
context.MapRoute(
"Edit_Comment",
"posts/{postId}/comments/{commentId}/edit",
new { controller = "Comment", action = "Edit" }
);
Then in your CommentController.cs you would have the following action:
public ActionResult Edit(int postId, int commentId)
{
// Do your edit logic then return an ActionResult
}

MVC Routes based on POST parameters

We have an a PHP application that we are converting to MVC. The goal is to have the application remain identical in terms of URLs and HTML (SEO and the like + PHP site is still being worked on). We have a booking process made of 3 views and in the current PHP site, all these view post back to the same URL, sending a hidden field to differentiate which page/step in the booking process is being sent back (data between pages is stored in state as the query is built up).
To replicate this in MVC, we could have a single action method that all 3 pages post to, with a single binder that only populates a portion of the model depending on which page it was posted from, and the controller looks at the model and decides what stage is next in the booking process. Or if this is possible (and this is my question), set up a route that can read the POST parameters and based on the values of the POST parameters, route to a differen action method.
As far as i understand there is no support for this in MVC routing as it stands (but i would love to be wrong on this), so where would i need to look at extending MVC in order to support this? (i think multiple action methods is cleaner somehow).
Your help would be much appreciated.
I have come upon two solutions, one devised by someone I work with and then another more elegant solution by me!
The first solution was to specify a class that extends MVcRouteHandler for the specified route. This route handler could examine the route in Form of the HttpContext, read the Form data and then update the RouteData in the RequestContext.
MapRoute(routes,
"Book",
"{locale}/book",
new { controller = "Reservation", action = "Index" }).RouteHandler = new ReservationRouteHandler();
The ReservationRouteHandler looks like this:
public class ReservationRouteHandler: MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var request = requestContext.HttpContext.Request;
// First attempt to match one of the posted tab types
var action = ReservationNavigationHandler.GetActionFromPostData(request);
requestContext.RouteData.Values["action"] = action.ActionName;
requestContext.RouteData.Values["viewStage"] = action.ViewStage;
return base.GetHttpHandler(requestContext);
}
The NavigationHandler actually does the job of looking in the form data but you get the idea.
This solution works, however, it feels a bit clunky and from looking at the controller class you would never know this was happening and wouldn't realise why en-gb/book would point to different methods, not to mention that this doesn't really feel that reusable.
A better solution is to have overloaded methods on the controller i.e. they are all called book in this case and then define your own custome ActionMethodSelectorAttribute. This is what the HttpPost Attribute derives from.
public class FormPostFilterAttribute : ActionMethodSelectorAttribute
{
private readonly string _elementId;
private readonly string _requiredValue;
public FormPostFilterAttribute(string elementId, string requiredValue)
{
_elementId = elementId;
_requiredValue = requiredValue;
}
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (string.IsNullOrEmpty(controllerContext.HttpContext.Request.Form[_elementId]))
{
return false;
}
if (controllerContext.HttpContext.Request.Form[_elementId] != _requiredValue)
{
return false;
}
return true;
}
}
MVC calls this class when it tries to resolve the correct action method on a controller given a URL. We then declare the action methods as follows:
public ActionResult Book(HotelSummaryPostData hotelSummary)
{
return View("CustomerDetails");
}
[FormFieldFilter("stepID", "1")]
public ActionResult Book(YourDetailsPostData yourDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[FormFieldFilter("stepID", "2")]
public ActionResult Book(RoomDetailsPostData roomDetails, RequestedViewPostData requestedView)
{
return View(requestedView.RequestedView);
}
[HttpGet]
public ActionResult Book()
{
return View();
}
We have to define the hidden field stepID on the different pages so that when the forms on these pages post back to the common URL the SelectorAttributes correctly determines which action method to invoke. I was suprised that it correctly selects an action method when an identically named method exists with not attribute set, but also glad.
I haven't looked into whether you can stack these method selectors, i imagine that you can though which would make this a pretty damn cool feature in MVC.
I hope this answer is of some use to somebody other than me. :)

Resources