Handle action with invalid Id parameter - asp.net-mvc

I am new to ASP.NET MVC and I wonder if the way I handled these cases is the most appropriate.
I have an "ArticleController", which has an action called "Details" (Used the auto-generate edit template).
By default, there is an optional id at the routing table,
and I want to know how to handle the cases when I don't receive any Id or when I receive a wrong id parameter.
In order to fix it I've wrote this (Note the DefaultValue attribute):
public ViewResult Details([DefaultValue(0)]int id)
{
Article article = db.Articles.Find(id);
if (article == null)
{
return View();
}
return View(article);
}
And at the view I've wrote this:
#if (Model == null)
{
<div>Wrong article id was given.</div>
}
else
{
// Handle as a normal case
}
You would have handled these cases differently? If yes, how?

I think the cleanest approach is to set up your routes so that when no ID is present, a user is routed to a different action. That's what the default route does. For example: /Articles/ will invoke ArticleController::Index(), and /Articles/4 will invoke ArticleController::Details(4).
As far as the case goes where an ID is not found, personally, I prefer to return a 404 error:
return new HttpNotFoundResult("This doesn't exist");

You can make your Id nullable like this:
public ViewResult Details(int? id)
If the user provides no id or an incorrect one, the id won't have a value which you can check with id.HasValue. If the id has a value, you can obtain it with id.Value.

Related

How to handle if an invalid Id is passed to the action

I am currently doing a project in MVC 3 and can't figure out if a user passes an invalid id (let's say 23233), how can i display a message to the user that item with this id does not exist?
Assuming this is ASP.NET, use Find() in your DbSet to find a user with that Id. If the result is null, use something like RedirectToAction() to send the user to a page explaining the problem.
The VS scaffolding system already does something similar, except it returns an HttpNotFound() instead in the automatically generated code. You can use its logic as a starting point.
first.
You are create a checker method for id.
public bool idChecker(string id)
{
try
{
double numeric = -1;
bool retval = double.TryParse(id, out numeric);
return retval;
}
catch (Exception)
{
return false;
}
}
and you will use idChecker method.
public ActionResult YourActionMethod(string id)
{
if (!idChecker(id))
return Content("Invalid ID"); // or your code
else
return View(); // or your code.
}

Asp.net mvc 3 handling unexpected query

I am just wondering about the situation where the user may request through unexpected query. Suppose i have the controller action
public ViewResult Details(int id)
{
Description description = db.Descriptions.Find(id);
return View(description);
}
The ideal query in the browser will be /admin/Details?id=1.
What if the user entered the id=-1 or id=a or any other unexpected inputs. How to handle this?
To ensure numeric values, you could add id = #"\d+" route constraint, and your action will be hit only if requested id is numeric, otherwise it will return http not found;
And in all other cases you should always check user input, something like this:
public ActionResult Details(int id)
{
Description description = db.Descriptions.Find(id);
if(description == null)
{
return new HttpStatusCodeResult(404);
}
return View(description);
}
And user will be notified that he requested resource with invalid identifier

MVC3 using routes or using controller logic?

I'm relatively new with MVC3, but I'm using it, C# and EF4 to create an application website. The routing that I'm using is the same as in the default Microsoft project created when I selected MVC3 pattern, nothing special:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new[] { "MySite.Controllers" }
);
}
And everything is working fine there. We're using the default Membership Provider, and users also get an INT value that identifies their account. This lets them see their profile pretty easily with a simple routing like:
www.mysite.com/profile/4
...for example. However, the client has asked that a lot of accounts be pre-generated and distributed to selected users. I've worked up a way to run that through SQL Server and it works fine, all the accounts got created (about a thousand). Additionally, I've add a bit field ('Claimed') that can help identify whether one of these pre-generated accounts has been 'activated' by these users.
My question is, when a user is given a link to come visit their (un-activated) account, should I use a test when doing the initial routing on that page to identify their account as un-claimed and send them somewhere else to finish entering details into their account? Or should I let them go to the same page as everyone else, and have something in the controller logic that identifies this record as un-claimed, and then send them to another page to finish entering details etc.? Is there a good reason for doing one over the other?
And what about people who make up (or have a typographical error) in their Id value, like:
www.mysite.com/profile/40000000000
(and the site only has a thousand users so far), should that be handled similarly, or through different means entirely? (I.e., in one scenario we're identifying an existing account that is not yet claimed, and in another scenario we're having to figure out that the account doesn't even exist.)
Any help would be greatly appreciated.
EDIT:
I'm trying to implement Soliah's suggested solution, and got stuck a bit on the fact that the if (id != 0) didn't like that the id might not be in an INT. I'm past that now, and attempting to figure out a way to do the check if valid portion, but possibly I have not solved the problem with the id not being treated as an INT? Something is definitely not right, even though I'm trying to convert it again during my database test for validity. Any ideas on why I'm getting the error below? What am I missing?
public class ValidProfileIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var id = (Convert.ToInt32(filterContext.ActionParameters["Id"]));
if (id != 0)
{
// Check if valid and behave accordingly here.
Profile profile = db.Profiles.Where(q => q.ProfileId == (Convert.ToInt32(id))).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
Cannot implicitly convert type 'System.Linq.IQueryable<Mysite.Models.Profile>' to 'Mysite.Models.Profile'. An explicit conversion exists (are you missing a cast?)
EDIT #2:
I'm working on Robert's suggestion, and have made partial progress. My code currently looks like this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
bool isActivated = // some code to get this state
return isActivated;
}
}
which I got to after reading the blog entry, and (believe it or not) this posting: http://pastebin.com/Ea09Gf4B
I needed to change ActionSelectorAttribute to ActionMethodSelectorAttribute in order to get things moving again.
However, what I don't see how to do is to get the Id value into the bool isActivated test. My database has a view ('Claimed') which can give back a true/false value, depending on the user's profile Id that it is handed, but I don't see where to add the Id. Would something like what Soliah edited work?
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
bool isActivated = db.Claimed.Where(c => c.ProfileId == id).FirstOrDefault();
EDIT #3:
Here is my current state of the code:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse((string)controllerContext.RouteData.Values["id"]);
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
bool isActivated = profile;// some code to get this state
return isActivated;
}
}
For me, I had to change things to int.Parse((string)controllerContext.RouteData.Values to get them to work, which they seem to do (to that point.) I discovered that formatting here: Bind a routevalue to a property of an object that is part of viewmodel
The line
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
errors on the db. section, with error message as follows:
Cannot access a non-static member of outer type 'MySite.Controllers.HomeController' via nested type 'MySite.Controllers.HomeController.UserAccountActivatedAttribute'
...which is something that I have diligently tried to figure out with MSDN and Stack, only to come up empty. Does this ring any bells?
Others have suggested many things already, but let me bring something else to the table here.
Action Method Selector
In order to keep your controller actions clean, you can write an action method selector attribute to create two simple actions:
[ActionName("Index")]
public ActionResult IndexNonActivated(int id)
{
...
}
[ActionName("Index")]
[UserAccountActivated]
public ActionResult IndexActivated(int id)
{
...
}
This way you don't deal with checking code in your actions keeping them really thin. Selector filter will make sure that correct action will get executed related to user account activation state.
You can read more about action selector attributes in my blog post but basically you'd have to write something similar to this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse(controllerContext.RouteData.Values["id"] ?? -1);
bool isActivated = // some code to get this state
return isActivated;
}
}
And that's basically it.
This will make it possible for users to access their profile regardless whether their account has been activated or not. Or maybe even deactivated at some later time... And it will work seamlessly in the background.
One important advantage
If you'd have two actions with different names (as Juraj suggests), one for active profiles and other for activation, you'd have to do the checking in both, because even active users would be able to access activation action:
profile/4 > for active profiles
profile/activate/4 > for inactive profiles
Both actions should be checking state and redirect to each other in case that state doesn't "fit". This also means that each time a redirection would occur, profile will get checked twice. In each action.
Action method selector will check profiles only once. No matter what state user profile is in.
I'd prefer to keep my controller thin and place this in an action filter that you can annotate on the Index action of the Profile controller.
public class ValidProfileIdAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActinExecutingContext filterContext) {
int id;
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
// Check if valid and behave accordingly here.
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
The OnActionExecuting method will be called before your controller's action.
In your controller:
[ValidProfileId]
public ActionResult Index(int id) {
...
}
I would suggest to have that logic in the controller, as once he/she is activated, they may be able to use the same link to access their profile.
Checking whether an account is activated or not is a part of application logic and should be implemented inside the controller (or deeper). Within the controller, you can redirect un-activated users to any other controller/action to finish the activation. URL routing mechanism should route simply according to a shape of an incoming URL and shouldn't contact the database.

MVC routing with optional parameter

I have this route set up:
routes.MapRoute(
"home3", // Route name
"home3/{id}", // URL with parameters
new {
controller = "home",
action = "Index",
id = UrlParameter.Optional } // Parameter defaults
);
But in my controller I don't know how to get the optional id parameter. Can someone explain how I can access this and how I deal with it being present or not present.
Thanks
your can write your actionmethod like
public ActionResult index(int? id)
{
if(id.HasValue)
{
//do something
}
else
{
//do something else
}
}
How to avoid nullable action parameters (and if statements)
As you've seen by #Muhammad's answer (which is BTW the one to be accepted as the correct answer) it's easy to get optional parameters (any route parameters actually) into controller actions. All you you have to make sure is that they're nullable (because they're optional).
But since they're optional you end up with branched code which is harder to unit test an maintain. Hence by using a simple action method selector it's possible to write something similar to this:
public ActionResult Index()
{
// do something when there's not ID
}
[RequiresRouteValues("id")]
public ActionResult Index(int id) // mind the NON-nullable parameter
{
// do something that needs ID
}
A custom action method selector has been used in this case and you can find its code and detailed explanation in my blog post. These kind of actions are easy to grasp/understand, unit test (no unnecessary branches) and maintain.

routing and URL structure

Is there anyway that I can exclude an ID field from the generated URL, but still be able to use the ID value? If you take a StackOverflow URL:
http://stackoverflow.com/questions/3409196/asp-net-mvc-routing-question
Can that URL be rendered without the Question ID?
You can display the question without an ID but question title has to be unique for every question.
Also you can still use the IDs for finding questions then redirect to another URL that only displays the question title. If that's how you wanna do it I can post an example.
Here's an example:
// this method finds a file from database using the id
//and passes the object with TempData
public ActionResult InitialDetail(int id)
{
var question = questionRepository.GetFile(id);
if (question==null)
return View("NotFound");
else
{
TempData["question"] = question;
return Redirect("/questions/" + question.Name);
}
}
//this method uses model passed from other method and displays it
public ActionResult Details(string questionName)
{
if (TempData["question"] == null)
{
return View("NotFound");
}
else
return View("Details", TempData["question"]);
}
You also have to define a route for this to work
routes.MapRoute("QuestionPage", //Files/id/fileName
"questions/{questionName}",
new { controller = "Questions", action = "Details" } );
Add this route just before the default route. It may mess things if you have routes for URL starting with http://domain.com/questions.
Note: This may not be the best solution. If your question titles are not unique, you can't put links with this structure in your page. First it has to look for question using ID.

Resources