Attribute Routing - optional parameter not working? - asp.net-web-api

According to http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx#optionals-and-defaults
You can have optional parameters by adding a question mark (?) when using attribute routing. However it does not work for me (ASP.NET Web API 5).
[Route("staff/{featureID?}")]
public List<string> GetStaff(int? featureID) {
List<string> staff = null;
return staff;
}
If I use staff/1 etc it works fine, if I use /staff I get the usual:
"No HTTP resource was found that matches the request URI..."
"No action was found on the controller that matches
the request."
Am I missing a reference or something? Or doing it wrong?

I also ran into the same issue and solved it a little differently. However, it still didn't work for me as laid out in that blog post. Instead of adding the default parameter value in the route definition I added it to the function definition.
I had to do this to for my example to work properly because I was using a string instead of an int and adding the default in the route definition of null caused my function parameter to have the string value "null".
[Route("staff/{featureID?}")]
public List<string> GetStaff(int? featureID = null) {
List<string> staff = null;
return staff;
}

This is because you always have to set a default value for an optional parameter, even if the default is null. That is why this works:
[Route("staff/{featureID=null}")]

If I do:
[Route("staff/{featureID=null}")]
instead of
[Route("staff/{featureID?}")]
It works.
Technically this doesn't answer my question but it gets me working!

Related

Web Api Routing: Multiple controller types were found that match the URL for parameter VS constant paths

My issue is similar to Web Api Routing : Multiple controller types were found that match the URL but I want to keep them in separate controllers.
From the comments, 2 preexisting answers are good workarounds but do not solve the actual issue I'm trying to resolve.
The URLs I'm making up are similar to nested directories in a file system OR are very similar to Firebase URLs.
/BiggestSet/{BiggestSetCode}/Subset1/{Subset1Code}/SubsetOfSubset1/{SubsetOfSubset1}
... etc all the way down to where ever the tree stops. Think of it as a tree of data.
/Collection/{Instance}/Collection/{Instance}
The issue I have is that at the /Collection level I want to also provide specific collection level operations. Like Add and search and other collection specific Operations Collection/ProccessData
Collection Controller:
/Collection/Add
/Collection/ProcessDataOnTheColleciton
Instance Controller:
/Collection/{InstanceCode}
/Collection/{InstanceCode}/ProcessOnTheInstance
The problem I'm having is the Collection/ProcessData clashes with the instance Collection/{InstanceCode}
NOTE: 1 is an parameter and the other is a constant.
If you setup the controllers so that collection and Instance are in the same controller. the /{InstanceCode} doesn't clash with the /ProcessData
BUT
If you setup so the controllers are split into logical functions WebAPI gives the error Multiple controller types were found that match the URL.
Does anyone know how to modify attribute routing to somehow behave as if they are in the same controller OR to prioritize the constant over the parameter across controllers?
To keep two separate controllers and still have such routes you can use regular expression route constraints. This way you can specify for the instanceCode you accept everything except the actions from the other controller.
Here is a sample of how to configure routes like that:
public class CollectionController : ApiController
{
[HttpGet]
[Route("Collection/Add")]
public string Add()
{
return $"CollectionController = Collection/Add";
}
[HttpGet]
[Route("Collection/Process")]
public string Process()
{
return $"CollectionController = Collection/Process";
}
}
public class InstanceController : ApiController
{
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}")]
public string Get(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}";
}
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}/Process")]
public string Process(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}/Process";
}
}
Here is also a link to the post that explains the regular expression used in the sample.
An even better option would be if you have a specific format for the instanceCode and set the regular expression to accept only this specific format. Then you would not need to modify the regular expression for every new action added. I include also a link to the documentation for all available Route constraints. There you can see all the available options. For example if your instance code is a number you don't even need a regular expression you can just restrict with the int constraint like this [Route("Collection/{instanceCode:int}")].

How to get Swagger UI to display similar Spring Boot REST endpoints?

I have a controller class with two endpoints
#RestController
#RequestMapping
public class TestController {
#RequestMapping(
value= "/test",
method = RequestMethod.GET)
#ResponseBody
public String getTest() {
return "test without params";
}
#RequestMapping(
value= "/test",
params = {"param"},
method = RequestMethod.GET)
#ResponseBody
public String getTest(#PathParam("param") int param) {
return "test with param";
}
}
One has a parameter, one doesn't, and the both work.
If I use curl or a web browser to hit the endpoints
http://localhost:8081/test
returns
test without params
and
http://localhost:8081/test?param=1
returns
test with param
but the swagger ui only shows the one without a parameter.
If I change the value in the request mapping for the request with a parameter to
#RequestMapping(
value= "/testbyparam",
params = {"param"},
method = RequestMethod.GET)
Swagger UI displays both endpoints correctly, but I'd rather not define my endpoints based on what swagger will or won't display.
Is there any way for me to get swagger ui to properly display endpoints with matching values, but different parameters?
Edit for Clarification:
The endpoints work perfectly fine; /test and /test?param=1 both work perfectly, the issue is that swagger-ui won't display them.
I would like for swagger ui to display the endpoints I have defined, but if it can't, then I'll just have to live with swagger-ui missing some of my endpoints.
Edit with reference:
The people answering here: Proper REST formatted URL with date ranges
explicitly say not to seperate the query string with a slash
They also said "There shouldn't be a slash before the query string."
The issue is in your Request Mapping, The second method declaration is overriding the first method. As Resource Mapping value is same.
Try changing the second method to below. As you want to give input in QueryParam rather than path variable, you should use #RequestParam not #PathParam.
Note that you have to give /test/, In order to tell Spring that your mapping is not ambiguous. Hope it helps.
#RequestMapping(
value= "/test/",
method = RequestMethod.GET)
#ResponseBody
public String getTest (#RequestParam("param") int param) {
return "test with param"+param;
}
Upon reading clarifications, the issue here is that swagger-ui is doing the correct thing.
You have two controller endpoints, but they are for the same RESOURCE /test that takes a set of optional query parameters.
Effectively, all mapped controller endpoints that have the same method (GET) and request mapping (/test) represent a single logical resource. GET operation on the test resource, and a set of optional parameters which may affect the results of invoking that operation.
The fact that you've implemented this as two separate controller endpoints is an implementation detail and does not change the fact that there is a single /test resource that can be operated upon.
What would be the benefit to consumers of your API by listing this as two separate endpoints in swagger UI vs a single endpoint with optional parameters? Perhaps it could constrain the set of allowed valid query parameters (if you set ?foo you MUST set &bar) but this can also be done in descriptive text, and is a much more standard approach. Personally, I am unfamiliar with any publicly documented api that distinguishes multiple operations for the same resource differentiated by query params.
As per Open API Specification 3
OpenAPI defines a unique operation as a combination of a path and an
HTTP method. This means that two GET or two POST methods for the same
path are not allowed – even if they have different parameters
(parameters have no effect on uniqueness).
Reference - https://swagger.io/docs/specification/paths-and-operations/
This was also raised as an issue but it was closed because OAS3 doesn't allow that -
https://github.com/springdoc/springdoc-openapi/issues/859
Try including the param in the path as below.
#GetMapping("/test/{param}")
public String getTest(#PathVariable final int param) {
return "test with param";
}
I'm unclear exactly what you're attempting to do, but I'll give two solutions:
If you want to have PATH parameters e.g. GET /test & GET /test/123 you can do:
#GetMapping("/test")
public String getTest() {
return "test without params";
}
#GetMapping("test/{param}")
public String getTest(#PathVariable("param") int param) {
return "test with param";
}
If you want query parameters (GET /test and GET /test?param=123) then you need a single endpoint that takes an optional parameter:
#GetMapping("test")
public String getTest(#RequestParam("param") Integer param) {
if(param == null) {
return "test without params";
} else {
return "test with param";
}
}

How do I return a template(thymeleaf) in spring boot and resolve it to a particular endpoint

Short: I want to use Thymeleaf template index.html but have the url point to thanks.html.
In depth: I am trying to have a form submission take my user to a page http://localhost:8080/thanks.html. I dont want the action of the form to be thanks.html for a few different reasons but I have greatly simplified the logic below. When all of the validation of the form are passed, I want to pass in a variable to indicate which layout to use. I have that working by using a model variable called contentPage. The problem is that if i have "return "thanks.html";" in the indexSubmit Method I get an error from thymeleaf saying template not found. If I change that to "return "index.html"; everything works but the url is http://localhost:8080/ instead of http://localhost:8080/thanks.html.
#PostMapping("/")
public String indexSubmit(Model model) {
model.asMap().clear();
model.addAttribute("contentPage","layout/thanks.html");
return "thanks.html";
}
#GetMapping("/thanks.html")
public String thanks(Model model) {
model.addAttribute("contentPage","layout/thanks.html");
return "index.html";
}
I fond an answer on my own:
return "redirect:thanks.html";
Thanks,
Brian

Attribute routing based on the existence of a querystring parameter

I would like two different methods to be called depending on whether the request is GET /things/123 or GET /things/123?preview
Obviously, this would be a workaround:
[RoputePrefix("api/things")]
...
[HttpGet, Route("{id}")]
public object GetThing(int id)
{
if (Request.RequestUri.Query == "?preview")
return GetPreview(id);
else
return GetFull(id);
}
...at which point it's almost like using bool preview = false as a parameter and adding ?preview=true to the request. But I want this syntax instead and would prefer to handle it with routing if possible.
For reasons not shown here, I can't do the next easiest thing, which is to use /preview as a suffix.

Manually parse urls in ASP.NET-MVC-3

I have a Controller with syntax like this:
public class CrudController<TEntity> : Controller
Now if I need a CrudController for Entity User, I just need to extend the CrudController like this
UserCrudController : CrudController<User>
Its working just fine. But, the thing is, the UserCrudController is simply empty. Also, there are some other CrudControllers which are empty too.
Now, I am looking for a way to avoid writing the empty crud controllers. I simply want to create Instances of CrudController with appropriate generic argument. Perhaps by a strict naming convention as described bellow.
The URL will be like: #Html.ActionLink("Create", "UserCrud")
When the URL will be received, it will try to locate the controller named UserCrud (The default thing)
If it fails to locate UserCrud, Crud<User> will be created.
Now, I can do the things I want to do. But exactly where do I do these? Where is the url parsed in mvc?
With help of Craig Stuntz's comment on the question and this question and its accepted answer, I have solved my problem.
I have implemented a custom CotrollerFactory
public class CrudControllerFactory : DefaultControllerFactory {
protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName) {
Type controllerType = base.GetControllerType(requestContext, controllerName);
if(controllerType == null) {
int indexOfEntityEnd = controllerName.LastIndexOf("Crud");
if(indexOfEntityEnd >= 0) {
string entityName = controllerName.Substring(0, controllerName.Length - indexOfEntityEnd - 1);
// Get type of the CrudController and set to controller tye
}
}
return controllerType;
}
}
And then in Application_Start(), I added this line:
ControllerBuilder.Current.SetControllerFactory(typeof(CrudControllerFactory));

Resources