Web API .net core Attribute routing with FromQuery - asp.net-core-webapi

I have below code implemented Web API (.net Framework 4.5.2). When I make a call "http://localhost:3000/123" - It fetches user details whose id is 123.
If I make "http://localhost:3000/Class1/?status=Active" - It fetches user details who belong to Class 1 and status as active. Same I converted to .net core and eventhough I mentioned FromQuery, call always goes to ":http://localhost:3000/123"
public class UserController : Controller
{
private Repository repository;
[HttpGet("{id}")]
public object Get(string id)
{
return repository.GetUser(id) ?? NotFound();
}
[HttpGet("{group}")]
public object Get(string group, Status status)
{
// Get the User list from the group and whose status is active
}
}
Please let me know how to resolve this without changing Route Parameter.

Simply, you have two conflicting routes here. There's no way for the framework to know which to route to, so it's just going to take the first one. As #Nkosi indicated, if there's some kind of constraint you can put on the param, that will help. You may not be able to restrict to just ints, but perhaps there's a particular regex, for example, that would only match one or the other. You can see your options for constraining route params in the relevant docs.
If there's no clear constraint you can apply that will not also match the other, then you're mostly out of luck here. You can simply change one of the routes to be more explicit, e.g. [HttpGet("group/{group}")]. If the route absolutely must be the same, your only other option is to have one action handle both cases, branching your code depending on some factor.
[HttpGet("{id}")]
public object Get(string id, Status? status = null)
{
if (status.HasValue)
{
// Note: treat `id` as `group` here.
// Get the User list from the group and whose status is active
}
else
{
return repository.GetUser(id) ?? NotFound();
}
}
That may not be the best approach (branching on presence of status), but it's just an example. You'd need to decide what would work best here.

Related

Delete WebApi FromURI binding

I am trying to create a .NET5 WebApi delete method in a controller class where this method receives several "ids" that will be used for deleting some entities.
I realized when building the delete request on the client side that specifying a content does not make sense. So I was guided to pass ids on the Uri, hence the use of the "FromUri" attribute:
// DELETE: api/ProductionOrders/5
[HttpDelete("ProductionOrders")]
public IActionResult DeleteProductionOrder([System.Web.Http.FromUri]int[] ids)
{
//code
}
If this is a reasonable approach, is there a better way to build this Uri from the client-side? Imagine instead of an array of ints I had a complex type. How can I serialized this and put into the Uri?
For this example I end up building up a URI like this:
http://localhost:51081/api/ProductionOrders?ids=25563&ids=25533
Personally, if I have to pass a List or a complex type I would map values from the Body via JSON. The DELETE allow using body. And then just decorate your param with [FromBody] attribute.
Despite some recommendations not to use the message body for DELETE requests, this approach may be appropriate in certain use cases.
This allows better extensibility in case you need to change how the data is coming.
In your case with ids I’d create new class like this:
public class RequestEntity {
[JsonPropertyName("Ids")]
public List<int> Ids { get; set; }
}
And then when calling this method, send the Body along with the request.
{
"Ids": [25392, 254839, 25563]
}
In a future you can pass complex objects just by changing what is send to server and implement complex logic.

Is there a better way to implement role based access in ASP.NET framework?

Basically I've spent the last few days trying to figure out how to add simple Admin and Member roles onto a website I'm developing for a friend. (I am using ASP.NET Framework 5.2.7.0). I know that Microsoft has a nice role based access feature built in which allows you to put something like [Authorize Role=("Admin") at the top of the controller; however I have not been able to get it to work at all and most of the resources I've found are for ASP.NET Core.
I've tried modifying my web.config file to enable the role based access (and hopefully migrate the roles and such to my database). But since I've been unable to figure any of this out, I've tried going a more hacky route. (**I am not an advanced programmer, I've been doing this for about a year now and am in no way a pro). This is what I've basically come up with in my attempt to verify if a user is an admin (which also didn't work).
[Authorize]
public class AdminController : Controller
{
private LDSXpressContext db = new LDSXpressContext();
public ActionResult AdminPortal()
{
IsAdmin();
return View();
}
private ActionResult IsAdmin()
{
string name = User.Identity.Name;
//The User.Identity.Name stores the user email when logged in
var currentUserObject = db.accounts.Where(x => x.clientEmail == name);
Account currentUser = new Account();
foreach (var user in currentUserObject)
{
//I loop through the results, even though only one user should
//be stored in the var CurrentUserObject because it's the only
//way I know how to assign it to an object and get its values.
currentUser = user;
}
if (currentUser.role == 2) //the number 2 indicates admin in my db
{
return null;
}
else
{
//Even when this is hit, it just goes back and returns the
//AdminPortal view
return RedirectToAction("Index", "Home");
}
}
}
Now I'm nearly positive that is is NOT a very secure way to check if a signed in user is an admin, but I was hoping that it would at least work. My idea was when someone attempted to access the AdminPortal, the IsAdmin method would run and check if the user is an admin in the database. If they are, then it returns null and the AdminPortal view is displayed, if they are not an Admin, then they are redirected to the Index view on the home page. However, the AdminPortal page is always displayed to any user and this doesn't seem to work either. I've even stepped into the code and watched it run over the return RedirectToAction("Index", "Home"); action, but then it jumps back to the AdminPortal method and just returns the AdminPortal view. So my question is:
1) If anyone happens to have experience with Role Based access in ASP.NET Framework, I would love some tips on how to get it set up
or,
2) If all else fails and I need to use my hacky method, why does it continue to return the AdminView even when the user is not an admin.
**Note: I know I could create a function that returns true or false if the user is an Admin or not, and then have an if/else statement in the AdminPortal controller that will return on view for true and another for false, however I don't want to have to implement that onto every ActionMethod, it'd be nice to keep it down to one line, or just the [Authorize Role="Admin] above the controller if possible.
Thank you guys so much for any help provided, I've been trying to research and fix this for days now and decided to reach out and ask the community!
At a minimum, you'll want to make some adjustments to what you're doing:
[Authorize]
public class AdminController : Controller
{
public ActionResult AdminPortal()
{
if(IsAdmin())
{
return View();
}
return RedirectToAction("Index", "Home");
}
private bool IsAdmin()
{
bool isAdmin = false;
using(LDSXpressContext db = new LDSXpressContext())
{
string name = User.Identity.Name;
//The User.Identity.Name stores the user email when logged in
// #see https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.singleordefault
var currentUser = db.accounts.SingleOrDefault(x => x.clientEmail.Equals(name, StringComparison.OrdinalIgnoreCase));
// If the email doesn't match a user, currentUser will be null
if (currentUser != null)
{
//the number 2 indicates admin in my db
isAdmin = currentUser.role == 2;
}
}
return isAdmin;
}
}
First off, DbContext instances are meant to used, at most, per the lifetime of an HTTP request. Moving it from the class / controller level and placing it within a using block makes sure that it's properly disposed.
Next, your IsAdmin function really just needs to return a true/false value based on your lookup, and then the AdminPortal action can decide what to do with that result.
Since email seems to be a unique field in your table, use the SingleOrDefault or FirstOrDefault LINQ extension to fetch a single matching record. Which one you use is up to you, but if it's truly a unique value, SingleOrDefault makes more sense (it will throw an exception if more than one row matches). Using the StringComparison flag with the String.Equals extension method makes your search case-insensitive. There are a few culture-specific versions of that, but ordinal matching is what I would normally use, here.
Implementing some version of the Identity framework is a bit too long for an answer here, but it's possible to implement a claims-based authentication scheme without too much work. That's something that probably needs a separate answer, though.

Default value for ViewModel

I'm making a simple Search page in MVC with some filters in it. The filters are represented by properties in my ViewModel. My ViewModel is binded to a GET form in the cshtml so my filter will appears in the querystrings and the user will be able to bookmark his search.
What I want to do is to assign a default value to some of my filters.
My (simplified) ViewModel :
public class SearchViewModel
{
//Filter I want to set a default value to
public OrganizationType? OrganizationType {get; set;}
//Results of the search
public IEnumerable<ItemViewModel> Items {get; set;}
}
I'd like to set a default value for OrganizationType. I can't simply set it in the constructor of SearchViewModel because it depends on the current user :
public void InitViewModel(SearchViewModel vm)
{
vm.OrganizationType = _someLogic.GetDefaultValue(_currentUser);
}
First solution was simply to check if OrganizationType is null, then assign a default value :
public ActionResult Search(SearchViewModel vm)
{
if(vm.OrganizationType == null)
vm.OrganizationType = _someLogic.GetDefaultValue(_currentUser);
return View(vm);
}
But this solution doesn't work as a null value corresponds to an empty filter and it's a choice that the user can make. So I can't override it.
The second solution I tried was to specify that the default value of the controller should be null in the Search action :
public ActionResult Search(SearchViewModel vm = null)
{
if (vm == null)
{
vm = new SearchViewModel();
InitViewModel(vm);
}
...
return View(vm);
}
But in practice, the variable vm is never null, so the default values are never setted.
I also tried having two Action, one wihout a ViewModel where I instanciate a new ViewModel with the default values and then call the second action :
public ActionResult Search()
{
var vm = new SearchViewModel();
InitViewModel(vm);
//Simply call the second action with the initizalied ViewModel
return Search(vm);
}
public ActionResult Search(SearchViewModel vm)
{
...
return View(vm);
}
But it doesn't work because there is now an ambiguity between the two action, and asp.net doesn't know which one to choose.
So in summary, I'd like to find a way to set a default value for a ViewModel, without setting it in the constructor and overriding user choices.
Another way to say it, how can I distinguish an "empty" ViewModel from one where some values are binded from the form.
Any idea ?
Ok I think I found a solution to my own problem...
I can use the ModelState property of the controler to check it the ViewModel is empty or was binded from the form :
public ActionResult Search(SearchViewModel vm = null)
{
if (ModelState.Count == 0)
{
InitViewModel(vm);
}
...
return View(vm);
}
So if ModelState.Count equals to 0 it means that user didn't change any filters. So the form is empty and we can bind our default values. As soon as the user will change one of the filters or submit the request, the ModelState.Count will be greater than 0 so we shouldn't set the default value. Otherwise we would override an user choice.
The logic of what you're doing is a little iffy. Generally speaking, if a value is nullable then null is the default value. However, it seems that you're trying to make a distinction here between whether the value is null because it's not set or null because the user explicitly set it to null. This type of semantic variance is usually a bad idea. If null has a meaning, then it should always carry that meaning. Otherwise, your code becomes more confusing and bugs are generally introduced as a result.
That said, you can't count on ModelState having no items. I've honestly never played around with ModelState enough in scenarios where there's not post data, but it's possible there's some scenario where there's no post data and yet ModelState may have items. Even if there isn't, this is an implementation detail. What if Microsoft does an update that adds items to ModelState in situations where it previously had none. Then, your code breaks with no obvious reason why.
The only thing you can really count on here is whether the request method is GET or POST. In the GET version of your action, you can reasonably assume that the user has made no modifications. Therefore, in this scenario, you can simply set the value to whatever you like without concern.
In the POST version of your action, the user has made some sort of modification. However, at this point, there is no way to distinguish any more whether the value is null because it is or because the user explicitly wanted it to be. Therefore, you must respect the value as-is.

How can I add multiple Get actions with different input params when working RESTFUL?

I'm trying to figure out whats the best way to have multiple Get actions in a REST controller.
I would like to do something like this:
Get By Id:
public ResponseType Get(Guid id)
{
// implementation
}
Get By Enum Type:
public ResponseType Get(EnumType type)
{
// implementation
}
Get By Other Enum Type:
public ResponseType Get(OtherEnumType otherType)
{
// implementation
}
etc..
Now when I do something like that, I get the next error message:
Multiple actions were found that match the request
I understand why I get the message and I was thinking how is the best way to do something like that (I want to stick with REST).
I know I can add a route like this:
routeTemplate: "api/{controller}/{action}/{id}"
But then I would need to change the action names and the urls - And this seems like a workaround when we are talking about rest.
Another thing I thought was to create multiple controllers with one Get - But that seems even wronger.
The third workaround was to handle one Get action with an input param that will have the state:
public ResponseType Get(ReqeustObj obj)
{
switch(obj.RequestType)
{
case RequestType.GetById:
// etc...
}
}
Anyway, I would like to know whats the best way to do something like that in REST (WebApi).
As you now, when Web API needs to choose an action, if you don't specify the action name in the route, it looks for actions whose name starts with the method name, GET in this case. So in your case, it will find multiple methods.
But it also try to match the parameters. So, if you include the parameters as part of the url (route parameters) or the query string, the action selector will be able to choose one of the available methods.
If you don't specify a parameter or specify the id in the url (or even in the query string) it should invoke the first overload. If you add the parameter name of the second action in the query string like this: ?type=VALUE it should choose the corresponding overload, and so on.
The question is that the parameter names must be different, or it will not be able to choose one or the other among all the overloads.
For example, if you use the urls in the comments in your browser, you'll see how the right method is chosen:
public class TestController : ApiController
{
// GET api/Test
public string Get()
{
return "without params";
}
// GET api/Test/5
public string Get(int id)
{
return "id";
}
// GET api/Test?key=5
public string Get(string key)
{
return "Key";
}
// GET api/Test?id2=5
public string Get2(int id2)
{
return "id2";
}
}
NOTE: you can also use route constraints to invoke differet methods without using query string parameters, but defining different route parameter names with different constraints. For example you could add a constraint for id accepting only numbers "\d+" and then a second route which accepts "key" for all other cases. In this way you can avoid using the query string

MVC RedirectToAction() any way to pass object to the target action?

The populated catList is always Count=0 when the code jumps to CreateProduct() so I take it it does not get delivered.
Considering RouteValueDictionary does not do this ? Any other way?
public ActionResult GetCats(int CatID)
{
List<Category> catList = new List<Category>();
if (CatID >= 0 )
{
catList = context.Categories.Where(x => x.PCATID == CatID).ToList();
}
return RedirectToAction("CreateProduct", "Admin", new { catList });
}
public ActionResult CreateProduct(List<Category> catList) { }
You are actually trying to use controllers to do data access.
Move the "GetCats" data retrieval into your business layer (Service object, Repository, whatever suits you).
Then, CreateProduct will need to be there twice (2 signatures). One with no parameters in which you are going to call "GetCats" from your business layer and send it to the view.
The other implementation is going to be the flagged with the HttpPostAttribute and will contain in parameters all the necessary information to create a cat.
That's all. Simple and easy.
You could place any items that you need in TempData then call RedirectToAction.
RedirectToAction simply returns a "302" code to the browser with the URL to redirect to. When this happens your browser performs a GET with that URL.
Your RouteValues need to be simple. You can't really pass complex object or collections using a route value.
if you don't care about the browser url changing you could just
return CreateProduct(catList)

Resources