Possible to trigger action depending on given params for MVC Controller? - c#

Say I have a controller called User, and I have a Index ActionResult, and then a secondary ActionResult called SetUserInfo. Example below:
public ActionResult Index(int id)
{
var enviorment = Zen.Components.Environment.GetEnvironmentByID(id);
return View(settingsViewModal);
}
//Second action
public ActionResult SetSite(int id, int siteID)
{
var enviorment =
Zen.Components.Environment.GetEnvironmentByID(
new EnviornmentQuery() {EnviormentID = id, SiteID = siteID);
return View(enviorment.Site);
}
Since the url "Settings?id=1" fires the ActionResult "Index", can I get "Settings?id=1&siteID=133" to then let the controller know it has to trigger ActionResult "SetSite" based on the params it was given, or do I have to make them optional in the first ActionResult, OR am I thinking of this all wrong. The route mapping is what is taking me a minute to fully get. I know it can be called as follows "Settings/SetSite?id=1&siteID=133", but wondering if I can do my prior example? If so, is it a bad way to handle it, or not?

You can map a route for it:
routes.MapRoute(
name: "SetSiteRoute",
url: "{controller}/{action}/{id}/{siteID}",
defaults: new { controller = "Settings", action = "SetSite" }
// Important part ^^^^^^^
);
Make sure you put it above your default route so that it takes precedence (routes are processed in order).
This will allow: www.site.com/Settings/1/500 where 1 is the id, and 500 is the siteID.

Although not exactly the answer you may be looking for..
First off, you cant map your route that way. The route cannot contain the ? character, so although you could use an alternative way to call your route using something like
/Settings/{id}/{siteId}
Using the following
/Settings?id={id}&siteID={siteId}
Cant be mapped as a route. However you could easily add the site id property to your Index action such as.
public ActionResult Index(int id, int? siteID)
{
if (siteID.HasValue)
return SetSite(id, siteID);
return null;
}
//Second action
public ActionResult SetSite(int id, int siteID)
{
return null;
}
I know this isnt exactly what you are looking for but will achieve the same result without having to mess around with your routes and urls. (Note i am just returning null so it compiles).
Cheers.

Related

Passing parameter to controller in MVC with no changes in url

I have a controller:
public ActionResult Index(string id)
{
ViewBag.ReloadedFromEmailForm = id;
return View();
}
The controller can be called from RouteConfig (as its default) with param id = null and can be called from some other controller which is returning
return RedirectToAction("Index", "Home", new {id = 1});
Now I want to get that id = 1 but in my url when I turn on app, to set just like regular without any param.
How to achieve that?
Now: localhost:8888/Index/1
What I want: localhost:8888/Index/ (but still I want to fill in ViewBag).
Solved. I didn't realize that property name id already in a route config,and automatically maps in url. All I needed to do was to change my prop to anything else, like foobar :)

Cannot generate correct links

I have the following code in a razor page:
#Url.Action("ArticleDetails", "Information", new { slug = article.Slug })
The page url where this code is placed has the form of http://localhost/category/6/category-name where 6 is the ID of the category
In the InformationController I have the following actions:
[HttpGet("article/{id}/{slug}")]
public IActionResult ArticleDetails(int id, string slug)
{
// some code ...
return View(data);
}
[HttpGet("article/{slug}")]
public IActionResult ArticleDetails(string slug)
{
// some code ...
return View(data);
}
How can I reach URL of form article/article-slug because #Url.Action(...) that I have in the page always try to reach controller action with id even if ID is not supplied as an anonymous type.
Links take the form of article/6/article-slug instead I want them to be article/article-slug without removing action with id in the controller.
I have noticed that 6 is from the id of the category. Also if I delete the controller action with Id i get the correct format of URL.
When resolving the action you're linking to, the IUrlHelper instance is using the current value of id in your current route (http://localhost/category/6/category-name), which has a value of 6, as you stated in your OP. Because there exists an ArticleDetails action that takes both an id and a slug (which you provide explicitly), the ArticleDetails action that takes both of these parameters is selected.
In order to resolve this, there are a couple of options. The first option is to clear out the RouteData value once you've used it in the action invoked when reaching http://localhost/category/6/category-name. In order to do that, you can use the following code within said action:
RouteData.Values.Remove("id");
I'm not a fan of doing it this way, but it does work. IMO, a better approach would be to simply use different names for the id parameter: e.g. categoryId and articleId in the respective controllers. This both fixes your issue and makes the code more readable in the corresponding actions. Your ArticleDetails action would simply change to:
[HttpGet("article/{articleId}/{slug}")]
public IActionResult ArticleDetails(int articleId, string slug)
{
// some code ...
return View(data);
}
Routing can be finicky when trying to do overloads like this. Your best bet is to use named routes:
[HttpGet("article/{id}/{slug}", Name = "ArticleDetailsIdSlug")]
public IActionResult ArticleDetails(int id, string slug)
[HttpGet("article/{slug}", Name = "ArticleDetailsSlug")]
public IActionResult ArticleDetails(string slug)
Then, in your view:
#Url.RouteUrl("ArticleDetailsSlug", new { slug = article.Slug })
Now, the routing framework doesn't have to try to figure out which route you actually want (and guess incorrectly apparently), as you'd told it exactly which route to use.

How to validate query string parameters in GET method

I have built an ASP.NET Core MVC application and I have used the default MVC URL routing:
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
I want to create the GET method for creating and editing the user.
For creating new user I need to go to url /Admin/User and for editing the existing user url /Admin/User/{id:int}
I have tried to create the method like this:
[HttpGet]
public async Task<IActionResult> User(int? id)
{
}
How can I restrict type of the id parameter?
I need only allow access to /Admin/User or /Admin/User/{id:int} - but if I try i.e. - /Admin/User/ABCD - (use an string as ID) - it is allowed as well.
If the parameter ID will be another type instead of number then I want to return 404.
The simplest option is to create a route with this constraint. Note that if you make the parameter required in the route like this, there is no reason to make id nullable.
[HttpGet("Admin/User")]
public async Task<IActionResult> Add()
{
}
[HttpGet("Admin/User/{id:int}")]
public async Task<IActionResult> Edit(int id)
{
}
Or keep the action methods named User and use:
app.UseMvc(routes =>
{
routes.MapRoute("AdminUser", "Admin/User/{id:int}");
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
Note that the convention-based approach doesn't differentiate between GET/POST.
Alternatively, if you want to make all of your ID parameters an int, you could configure the routing for the whole site like this:
routes.MapRoute(
name: "defaultWithId",
template: "{controller}/{action}/{id:int}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}");
Reference: Routing in ASP.NET Core
You would have to write a custom model binder for this, or create an overload for every other type which returns the 404. I would simply rethink your design and have a different /Admin/CreateUser action for creating a new user instead and just not allow the User call without an ID.
It is possible to achieve using single method.
If you are using numeric ids then for existing user id will be greater than zero. For new user Id will be zero or less than zero.
If you are using string ids then for existing user, id will be non-empty string. E.g. ABCD. For new user Id will be empty.
Based on this logic you can you can send appropriate response or 404 response.
If you are using string Ids, then change the data type of Get method. Instead of using nullable int use string.
Following code might help
[HttpGet("{id}")]
public async Task<IActionResult> User(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
//Replace NewUser view name by Appropriate View name in your project
return View("NewUser");
}
else
{
var isValidUser= IsValidUser(id);
if(isValidUser)
{
//Replace ExistingUser view name by Appropriate View name in your project
return View("ExistingUser");
}
else
{
//User Appropriate overload of NotFound
return NotFound();
}
}
}
private bool IsValidUser(string userId)
{
//Your logic to validate the existing user.
}

Query string not working while using attribute routing

I'm using System.Web.Http.RouteAttribute and System.Web.Http.RoutePrefixAttribute to enable cleaner URLs for my Web API 2 application. For most of my requests, I can use routing (eg. Controller/param1/param2) or I can use query strings (eg. Controller?param1=bob&param2=mary).
Unfortunately, with one of my Controllers (and only one), this fails. Here is my Controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{sport}/{drink}")]
public List<int> Get(string name, string sport, string drink)
{
// Code removed...
}
[HttpGet]
[Route("{name}/{drink}")]
public List<int> Get(string name, string drink)
{
// Code removed...
}
}
When I make a request to either using routing, both work fine. However, if I use a query string, it fails, telling me that that path does not exist.
I have tried adding the following to my WebApiConfig.cs class' Register(HttpConfiguration config) function (before and after the Default route), but it did nothing:
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So for clarity, I would like to be able to do both this:
localhost:12345/1/Names/Ted/rugby/coke
localhost:12345/1/Names/Ted/coke
and,
localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke
localhost:12345/1/Names?name=Ted&drink=coke
but sadly the query string versions don't work! :(
Updated
I've removed the second Action altogether and now trying to use just a singular Action with optional parameters. I've changed my route attribute to [Route("{name}/{drink}/{sport?}")] as Tony suggested to make sport nullable, but this now prevents localhost:12345/1/Names/Ted/coke from being a valid route for some reason. Query strings are behaving the same way as before.
Update 2
I now have a singular action in my controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
}
but still, using query strings does not find a suitable path, while using the routing method does.
I was facing the same issue of 'How to include search parameters as a query string?', while I was trying to build a web api for my current project. After googling, the following is working fine for me:
Api controller action:
[HttpGet, Route("search/{categoryid=categoryid}/{ordercode=ordercode}")]
public Task<IHttpActionResult> GetProducts(string categoryId, string orderCode)
{
}
The url I tried through postman:
http://localhost/PD/search?categoryid=all-products&ordercode=star-1932
http://localhost/PD is my hosted api
After much painstaking fiddling and Googling, I've come up with a 'fix'. I don't know if this is ideal/best practice/plain old wrong, but it solves my issue.
All I did was add [Route("")] in addition to the route attributes I was already using. This basically allows Web API 2 routing to allow query strings, as this is now a valid Route.
An example would now be:
[HttpGet]
[Route("")]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
This makes both localhost:12345/1/Names/Ted/coke and localhost:12345/1/Names?name=Ted&drink=coke valid.
With the Attribute routing you need to specify default values so they would be optional.
[Route("{name}/{sport=Football}/{drink=Coke}")]
Assigning a value will allow it to be optional so you do not have to include it and it will pass the value to specify.
I have not tested the query string for this but it should work the same.
I just re-read the question and I see that you have 2 Get verbs with the same path, I believe this would cause conflict as routing would not know which one to utilize, perhaps using the optional params will help. You can also specify one can be null and do checking in the method as to how to proceed.
[Route("{name}/{sport?}/{drink?}")]
Then check the variables in the method to see if they are null and handle as needed.
Hope this helps, some? lol
If not perhaps this site will, it has more details about attribute routing.
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Clip from that site:
Optional parameters and default values You can specify that a
parameter is optional by adding a question mark to the parameter, that
is:
[Route("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter
for action selection to succeed, but we can investigate lifting that
restriction. (Please let us know if this is important.)
Default values can be specified in a similar way:
[Route("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after
inline constraints in the parameter definition.
Just a side note from my part as well. In order for queryString params to work, you need to provide a default value for your method parameters to make it optional. Just as you would also do when normally invoking a C# method.
[RoutePrefix("api/v1/profile")]
public class ProfileController : ApiController
{
...
[HttpGet]
[Route("{profileUid}")]
public IHttpActionResult GetProfile(string profileUid, long? someOtherId)
{
// ...
}
...
}
This allows me to call the endpoint like this:
/api/v1/profile/someUid
/api/v1/profile/someUid?someOtherId=123
Using Route("search/{categoryid=categoryid}/{ordercode=ordercode}") will enable you to use both Querystrings and inline route parameters as answered by mosharaf hossain. Writing this answer as this should be top answer and best way. Using Route("") will cause problems if you have multiple Gets/Puts/Posts/Deletes.
Here's a slight deviant of #bhargav kishore mummadireddy's answer, but an important deviation. His answer will default the querystring values to an actual non-empty value. This answer will default them to empty.
It allows you to call the controller through path routing, or using the querystring. Essentially, it sets the default value of the querystring to empty, meaning it will always be routed.
This was important to me, because I want to return 400 (Bad Request) if a querystring is not specified, rather than having ASP.NET return the "could not locate this method on this controller" error.
[RoutePrefix("api/AppUsageReporting")]
public class AppUsageReportingController : ApiController
{
[HttpGet]
// Specify default routing parameters if the parameters aren't specified
[Route("UsageAggregationDaily/{userId=}/{startDate=}/{endDate=}")]
public async Task<HttpResponseMessage> UsageAggregationDaily(string userId, DateTime? startDate, DateTime? endDate)
{
if (String.IsNullOrEmpty(userId))
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(userId)} was not specified.");
}
if (!startDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(startDate)} was not specified.");
}
if (!endDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(endDate)} was not specified.");
}
}
}
I use FromUri attribute as solution
[Route("UsageAggregationDaily")]
public async Task<HttpResponseMessage> UsageAggregationDaily([FromUri] string userId = null, [FromUri] DateTime? startDate = null, [FromUri] DateTime? endDate = null)
Since you have [Route("{name}/{drink}/{sport?}")] as attribute routing, this code will never be hit.
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So only the attribute route [Route("{name}/{drink}/{sport?}")] is going to be honored here. Since your request localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke, doesn't have name, sport or drink in the URL it is not going to match this attribute route. We do not consider the query string parameters when matching the routes.
To solve this, you need to make all 3 optional in your attribute route. Then it will match the request.

Passing parameters through the url using MVC?

I know you can pass in parameters via urls like .com/MyPage/?controlID=5 but how can you do it with something like .com/MyPage/5? Thus not requiring the variable name or a question mark.
You would define a custom route, or use the model binding to get the intended effect. In your case, the route would be something like:
routes.Add("someRoute",
"{controller}/{action}/{controlId}",
new { controller = "Home", action = "Index", controlId = UrlParameter.Optional }
);
public ActionResult Index(int? controlId)
{
}
Now, the only "gotcha" with this route is that if you also have the default route specified, these two routes will be in contention and the first one you have defined will win. If there is some form of differentiating value (say, that controlId always matches some kind of pattern), then you can always add a HttpRouteConstraint to the route to differentiate your new route from the default route.
Alternatively, you can rename the parameter on your action method, if you are still using the default route, to be id, and change your query string key to 'id':
public ActionResult Index(int? id)
{
// Do Stuff
}
Create a method in MyPageController:
public ActionResult Index (int id)
{
}
That will work with the default routes

Categories