I have asp.mvc project. Currently routing is set to form following url's:
site/school/45
site/school/45/courses
site/school/45/teachers
site/school/45/courses/17
etc.
I want to change this '45' (id of the school) to more 'user friendly' name, for example to take 'Name' of school '45'.
Now action methods are like:
public ActionResult Index(int schoolId)
{ ...
Is it possible to change routing to display school name instead schoolID without changing action methods?
"without changing action methods" seems to be the most important part of your question, right?
Yes, you can. You may override OnActionEcecuting(ActionExecutingContext filterContext) method in your controller class to do name-id mapping for you. You need to do the following:
Implement base controller class, something like this:
public class BaseController: Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var shoolName = filterContext.RouteData.Values["schoolName"].ToString();
var schoolId = ...; // Get school id from name somehow here
filterContext.RouteData.Values.Add("schoolId", schoolId);
}
}
(I assume here that you will change your corresponding routing parameter name from schoolId to schoolName in Global.asax.cs / RegisterRoutes).
Update all your controllers by making them inherit not from Controller but from BaseController, like this:
public class SomethingController: BaseController
{
...
}
After that everything should be working. You won't need to change your controller actions - schoolId parameter will be already at place when it will come to calling them.
You will need to add a new route to the global routing to allow for a string name rather than just an int id.
The best option would be to create a new method that takes the school name and then (from the database or a cached object/dictionary) translates the name to the id then forwards to the int index method.
Something like this (not shown caching)
public ActionResult Index(string schoolName)
{
var schoolId = SomeMethodThatGetsTheIdFromString(schoolName)
RedirectToAction("Index", new { id = schoolId });
}
You could achieve this with a custom model binder to figure out the id of the school based on the name and use this to 'bind' to your schoolId parameter.
However, I would suggest leaving your routing the way it is - if a school's name changes for example it would mean that all bookmarks would no longer work, or your url would appear incorrect.
Related
What's the benefit of setting an alias for an action method using the "ActionName" attribute? I really don't see much benefit of it, in providing the user the option to call an action method with some other name. After specifying the alias, the user is able to call the action method only using the alias. But if that is required then why doesn't the user change the name of the action method rather then specifying an alias for it?
I would really appreciate if anyone can provide me an example of the use of "ActionName" in a scenario where it can provide great benefit or it is best to use.
It allows you to start your action with a number or include any character that .net does not allow in an identifier. - The most common reason is it allows you have two Actions with the same signature (see the GET/POST Delete actions of any scaffolded controller)
For example: you could allow dashes within your url action name http://example.com/products/create-product vs http://example.com/products/createproduct or http://example.com/products/create_product.
public class ProductsController {
[ActionName("create-product")]
public ActionResult CreateProduct() {
return View();
}
}
It is also useful if you have two Actions with the same signature that should have the same url.
A simple example:
public ActionResult SomeAction()
{
...
}
[ActionName("SomeAction")]
[HttpPost]
public ActionResult SomeActionPost()
{
...
}
I use it when the user downloads a report so that they can open their csv file directly into Excel easily.
[ActionName("GetCSV.csv")]
public ActionResult GetCSV(){
string csv = CreateCSV();
return new ContentResult() { Content = csv, ContentEncoding = System.Text.Encoding.UTF8, ContentType = "text/csv" };
}
Try this code:
public class ProductsController
{
[ActionName("create-product")]
public ActionResult CreateProduct()
{
return View("CreateProduct");
}
}
It is also helpful when you need to implement method overloading.
public ActionResult ActorView()
{
return View(actorsList);
}
[ActionName("ActorViewOverload")]
public ActionResult ActorView(int id)
{
return RedirectToAction("ActorView","Home");
}
`
Here one ActorView accepts no parameters and the other accepts int.
The first method used for viewing actor list and the other one is used for showing the same actor list after deleting an item with ID as 'id'.
You can use action name as 'ActorViewOverload' whereever you need method overloading.
This class represents an attribute that is used for the name of an action. It also allows developers to use a different action name than the method name.
In my route I have something like this:
controller/action/{id}
To my knowledge this means it will call any action with the parameter id like the following:
public ActionResult Detail(string id)
{
}
What do I have to do to make the following work without registering the particular route in global.asax file:
public ActionResult Detail(string customerId)
{
}
If you really don't want to rename the method parameter, you can use BindAttribute to tell MVC what its logical name should be:
public ActionResult Detail([Bind(Prefix = "id")] string customerId)
You can also pass customerId as query string, which is normally what I do:
<%: Html.ActionLink("Detail", "Detail", new { #customerId = Model.CustomerID}, null)%>
MVC does not enforce the routing, but rather try to resolve routing based on your url AND query string.
have a route like - controller/action/{customerId} or just rename the parameter customerId to id and then use it the particular way you want.
I've created a routing structure whereas the action part of the URL serves as a dynamic handler for picking a specific user created system name. i.e.
http://mysite.com/Systems/[SystemName]/Configure, where [SystemName] designates the name of the system they would like to configure.
The method that routes the system is the following:
public ActionResult Index(string systemName, string systemAction)
{
ViewData["system"] = _repository.GetSystem(systemName);
if (systemAction != "")
{
return View(systemAction);
}
else
{
// No Id specified. Go to system selection.
return View("System");
}
}
The above method sets the system to configure and routes to a static method where the view is displayed and a form awaits values.
The question I have is that when I create my configuration view, I lose my posted values when the form is submitted because it routes back to the above Index controller. How can I determine if data is being posted when hitting my above Index controller so that I can make a decision?
Thanks!
George
Annotate the controller method that handles the POST like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(string systemName, string systemAction)
{
// Handle posted values.
}
You can have a different method in your controller that handles the GETs:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Index(string systemName, string systemAction)
{
// No posted values here.
}
Note that, although I have copied the same method and parameters in each case, the signature for the second method (parameters and types) will have to be different, so that the two methods are not ambiguous.
The NerdDinner tutorial has examples of this.
I have bunch of action-methods that need to verify the ownership of the orderId passed to the action something like:
public ActionResult CancelOrder(int orderId) {
If (!MyDatabase.VerifyOwnership(orderId, User.Identity.Name) return View("You are an imposter!");
// ...
}
What's an easy way to verify orderId belongs to User.IdentityName without having to copy/paste same lines over and over?
I have tried ActionFilterAttribute but it doesn't have access to the context (MyDatabase object for example). What's a good way to handle this?
" but it doesn't have an access to the context"
Sure it does:
public class VerifyOwner : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var myController = (MyControllerType)filterContext.Controller;
if (!myController.MyDatabase.VerifyOwnership(orderId, User.Identity.Name)
//do what you do
base.OnActionExecuting(filterContext);
}
}
All you have to do is cast the Controller property to your controller type. This get really easy is you have a custom base Controller all your Controllers inherit from. Then set that base controller to have the MyDatabase property and you have an easy time using this attribute across multiple controllers.
Your controller seems to have access to your context. Therefore if you use an action filter attribute that implements IAuthorizationFilter you can cast the filterContext.Controller in the OnAuthorization method to your controller type and be able to do what you set out to in the first place. (Which I reckon is the way to go!)
Kindness,
Dan
I've seen a great answer to a similar question which explains, by inheriting all controllers from a new base class decorated with your own ActionFilter attribute, how you could apply some logic to all requests to your site.
I'd like to find a way to do that based on the area of a site my user is visiting.
For example, I will have a Product controller with a View action but I want to allow that to be used for the two following urls:
/Product/View/321 - display product id 321 to 'normal' users
/Admin/Product/View/321 - use the same View controller but spit out extra functionality for my admin users.
I could pass "admin" in as a parameter named "user" into my view action on my product controller to show extra information for administrators, a method for doing that is shown here. But what I'd then need to do is confirm my user was allowed to view that url. I don't want to decorate my Product controller with an ActionAttribute that checks for authentication because when unauthenticated users (and logged in administrators) view it at /Product/View/321, I want them all to see the standard view.
So what I'd like to do, is described below in pseudo-code:
When a url in the format "{userlevel}/{controller}/{action}/{id}" is called, I'd like like to call another controller that does the authentication check and then 'chain' to the original {controller} and pass through the {action}, {id} and {userlevel} properties.
How would I do that?
(I know that the over-head for doing a check on every call to the controller is probably minimal. I want to do it this way because I might later need to do some more expensive things in addition to user authentication checks and I'd prefer to only ever run that code for the low-traffic admin areas of my site. There seems no point to do these for every public user of the site)
At first I thought this might be as simple as adding a new route like this:
routes.MapRoute(
"Admin",
"Admin/{*pathInfo}",
new { controller="Admin", action="Index", pathInfo="" }
);
and then have a controller something like this:
public class AdminController : Controller
{
public ActionResult Index(string pathInfo)
{
//Do admin checks, etc here....
return Redirect("/" + pathInfo);
}
}
However, unfortunately all the options you have available in order to do the redirect (i.e. Redirect, RedirectToAction & RedirectToRoute) all do a 302 style redirect. Basically this means that your /Admin/Product/Whatever will execute & then bounce back to the browser telling it to redirect to /Product/Whatever in a totally new request, which means you've lost your context. I don't know of a clean way of keeping the redirect server side (i.e. like a Server.Transfer of old), apparently neither does the SO community...
(obviously, this is a non-solution, since it doesn't solve your problem, but I thought I'd put it here anyway, in case you could use the ideas in some other way)
So, what's an actual solution to the problem then? Another idea is to use an ActionFilter (yes I know you said you didn't want to do so, but I think the following will serve your purposes). Add a new route like this:
routes.MapRoute(
"Admin",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "", userLevel = "Admin" }
);
and then add an ActionFilter like this (that you could apply to all requests via a base controller object as you mentioned):
public class ExtendedAdminViewAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
object userLevel = filterContext.RouteData.Values["userLevel"];
if (userLevel != null && userLevel.ToString() == "Admin")
{
//Do your security auth checks to ensure they really are an admin
//Then do your extra admin logic...
}
}
}
So although it is using an ActionFilter that will apply to all requests, the only extra work done in most normal cases (i.e. a request for /Product/Whatever), is a single check of that bit of route data (userLevel). In other words, you should really see a performance hit for normal users since you're only doing the full auth check and extra admin work if they requested via /Admin/Product/Whatever.
1) Can't you just check for the role within the view?
<% if (HttpContext.Current.User.IsInRole ("Administrator")) { %>
// insert some admin specific stuff here
<%= model.ExtraStuff %>
% } %>
You can perform the same check in the controller if you need to set admin specific view model properties. In your controller you can do your extra processing only when the user is already authenticated:
public ActionResult Details (int productId)
{
ProductViewModel model = new ProductViewModel ();
if (User.Identity.IsAuthenticated && User.IsInRole ("Administrator"))
{
// do extra admin processing
model.ExtraStuff = "stuff";
}
// now fill in the non-admin specific details
model.ProductName = "gizmo";
return View (model);
}
The only thing missing here is a redirect to your login page when an admin tries to access the view without being authenticated.
2) Alternatively if you want to reuse your default product view with some extra bits you could try the following:
public class AdminController
{
[Authorize(Roles = Roles.Admin)]
public ActionResult Details(int productId)
{
ProductController productController = new ProductController(/*dependencies*/);
ProductViewModel model = new ProductViewModel();
// set admin specific bits in the model here
model.ExtraStuff = "stuff";
model.IsAdmin = true;
return productController.Details(productId, model);
}
}
public class ProductController
{
public ActionResult Details(int productId, ProductViewModel model)
{
if (model == null)
{
model = new ProductViewModel();
}
// set product bits in the model
return Details(model);
}
}
NOTE: I would prefer solution 1) over 2) due to the fact that you need to create a new instance of ProductController and that brings up it's own set of issues especially when using IoC.
You can solve this fairly easily by creating a base controller class which checks the user level in OnActionExecuting and, if authorized, sets a Role property to the same value and adds a "Role" entry to ViewData for use in the view. You can use this as a base class for all of your controllers and they will all have access to the Role property and all your views will have a "Role" entry added to ViewData:
public abstract class BaseController : Controller
{
public string Role { get; protected set; }
protected override void OnActionExecuting( ActionExecutingContext filterContext )
{
base.OnActionExecuting( filterContext );
Role = string.Empty;
string role = string.Empty;
object value;
if ( filterContext.RouteData.Values.TryGetValue( "role", out value ) )
role = value as string ?? string.Empty;
if ( filterContext.HttpContext.User.IsInRole( role ) )
Role = role.ToLowerInvariant();
ViewData[ "role" ] = Role;
}
}
Change the default route in Global.asax.cs:
routes.MapRoute(
"Default",
"{role}/{controller}/{action}/{id}",
new { role = "", controller = "Home", action = "Index", id = "" }
);
Now, in your controller actions, check the Role property for e.g. "admin" and, if so, add any necessary view data for the admin functions.
Render your admin UI using partials and in your view, check the role and call RenderPartial:
<% if ( Equals( ViewData[ "role" ], "admin" ) )
Html.RenderPartial( "_AdminFunctions" ); %>
<p>
This is the standard, non Admin interface...
</p>
This is an "outside the box" answer:
What about leveraging the policy injection block in entLib? With that you could create a policy that would run a "pre-method" on your action. Your pre-method could perhaps handle your problem.