I have a C# Web API with a few HttpGET and HttpPOST-methods in my API-Controllers. Here below a list of all of them to know what I want to achieve:
ProductsController:
// GET: {host}/api/products/all/{device_id}/{date}
[ActionName("all")]
public IEnumerable<ProductsAPI> GetAllProducts(string device_id, string date);
// GET: {host}/api/products/one/{device_id}/{date}/{id}
[ActionName("one")]
public ProductsAPI GetProduct(string device_id, string date, int id);
// POST: {host}/api/products/save/{device_id}
[HttpPost]
[AllowAnonymous]
[ActionName("save")]
public bool SaveProductChanges([FromBody] Data data, [FromUri] string device_id);
CategoriesController:
// GetAllCategories & GetCategory similar to ProductsController
// (included date string)
OrdersController:
// GetAllOrders & GetOrder & SaveOrderChanges similar to ProductsController
// (included date string)
TagsController:
// {host}/api/tags/all/{device_id}
[ActionName("all")]
public IEnumerable<TagsAPI> GetAllTags(string device_id);
// {host}/api/tags/one/{device_id}/{id}
[ActionName("one")]
public TagsAPI GetTag(string device_id, int id);
UsersController:
// GetAllUsers & GetUser similar to TagsController
// (excluding the need for a date string)
TestController:
// GET {host}/api/test/enable/{device_id}
[HttpGet]
[ActionName("enable")]
public String enableTests(string device_id);
// GET {host}/api/test/disable/{device_id}
[HttpGet]
[ActionName("disable")]
public String disableTests(string device_id);
Before this change I didn't included the Date-string in the Products, Categories and Orders and everything worked perfect when I used the following WebApiConfig's MapHttpRoute:
config.Routes.MapHttpRoute(
name: "DefaultApiRoute",
routeTemplate: "api/{controller}/{action}/{device_id}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Now my question is: How should I define my MapHttpRoute now that I've added the Date to some (not all) of my API-Controllers?
I did try this:
config.Routes.MapHttpRoute(
name: "ApiById",
routeTemplate: "api/{controller}/{action}/{device_id}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"^[0-9]+$" }
);
config.Routes.MapHttpRoute(
name: "ApiByDate",
routeTemplate: "api/{controller}/{action}/{device_id}/{date}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { date = @"d{2}-d{2}-d{4}" /* dd-MM-yyyy */, id = @"^[0-9]+$" }
);
But now none of my HttpGets are working anymore.. ('Server Error in '/' Application. The resource cannot be found.') Both with date ({host}/api/products/all/{device_id}/07-08-2014
) and without date ({host}/api/users/all/{device_id}
)..
Does anyone know what is wrong with my MapHttpRoutes and how I should change it?
EDIT 1:
I've changed the date constraints to date = @"^\d{2}-\d{2}-\d{4}$" /* dd-MM-yyyy */
like @Yevgeniy.Chernobrivets suggested. I've also tried his other suggestion of removing the constraints: new { id = @"^[0-9]+$" }
from the top MapHttpRoute. When I do this my requests like {host}/api/users/all/{device_id}
are working again, but the ones with the date like {host}/api/orders/all/{device_id}/07-08-2014
are giving me the following error:
{"$id":"1","Message":"No HTTP resource was found that matches the request URI
'http://localhost:*my_port*/api/orders/all/*given_device_id*/07-08-2014'.",
"MessageDetail":"No action was found on the controller 'Orders' that matches the request."}
EDIT 2:
I've also tried a single MapHttpRoute like so:
config.Routes.MapHttpRoute(
name: "WebApiRoute",
routeTemplate: "api/{controller}/{action}/{device_id}/{all_id}/{one_id}",
defaults: new { all_id = RouteParameter.Optional, one_id = RouteParameter.Optional },
constraints: new { all_id = @"^((\d{2}-\d{2}-\d{4})|([0-9]+))$", one_id = @"^[0-9]+$" }
);
With the same calls as before, but with the parameter names changed (The ones using date [Products, Categories, Orders] have date
changed to all_id
and id
changed to one_id
. The ones that aren't using date [Users, Tags] have id
changed to all_id
.)
But now none of my HttpGETs are working (once again) anymore (Server Error in '/' Application. The resource cannot be found.
). (PS: I know I'm pretty bad at regex and MapHttpRoutes, so does anyone know a complete solution so all my HttpGets and HttpPosts are working, of all API-Controllers?)
EDIT 3:
There seems to be something wrong with the constraints.. It almost looks like when constraints are added in the MapHttpRoute, the RouteParamter.Optional
is ignored.. (I probably did something wrong though, but can't figure out what.) I tried adding a MapHttpRoute before my original MapHttpRoute, this time without constraints like so:
config.Routes.MapHttpRoute(
name: "WebApiRoute",
routeTemplate: "api/{controller}/{action}/{device_id}/{all_id}/{one_id}",
defaults: new { all_id = RouteParameter.Optional, one_id = RouteParameter.Optional }//,
//constraints: new { all_id = @"^((\d{2}-\d{2}-\d{4})|([0-9]+))$", one_id = @"^[0-9]+$" }
);
config.Routes.MapHttpRoute(
name: "DefaultApiRoute",
routeTemplate: "api/{controller}/{action}/{device_id}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Now almost everything is working:
{host}/api/products/all/{device_id}/07-08-2014 // works
{host}/api/products/one/{device_id}/07-08-2014/5 // works
{host}/api/users/all/{device_id} // works
{host}/api/users/one/{device_id}/5 // doesn't work (gives same result as "users/all/{device_id}")
{host}/api/users/one/{device_id}/bla/5 // works (but shouldn't work..)
// Haven't tested the HttpPOSTs yet
Still, would prefer it if I could add constraints..
But even with my old code (before date was ever applied), constraints doesn't work when I add them:
config.Routes.MapHttpRoute(
name: "DefaultApiRoute",
routeTemplate: "api/{controller}/{action}/{device_id}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { id = @"^[0-9]+$" }
);
Without the constraints this works:
{host}/api/users/all/{device_id} // works
{host}/api/users/all/{device_id}/5 // works
With the constraints this doesn't work..:
{host}/api/users/all/{device_id} // doesn't work (error: "Server Error in '/' Application. The resource cannot be found. ")
{host}/api/users/all/{device_id}/5 // partly works (gives all users, but shouldn't need "/5" for this..)
{host}/api/users/one/{device_id}/5 // doesn't work (error: "No action was found on the controller 'Users' that matches the request.")
So I'm pretty confused about constraints' impact in the MapHttpRoutes.. :S And I'm still no where near a working solution after all these trial and errors I did..
Edit 4:
After another suggestion of @Yevgeniy.Chernobrivets I tried to separate All and One:
config.Routes.MapHttpRoute(
name: "ApiAll",
routeTemplate: "api/{controller}/all/{device_id}/{date}",
defaults: new { date = RouteParameter.Optional },
constraints: new { date = @"^\d{2}-\d{2}-\d{4}$", httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiOne",
routeTemplate: "api/{controller}/one/{device_id}/{id}/{date}",
defaults: new { date = RouteParameter.Optional },
constraints: new { date = @"^\d{2}-\d{2}-\d{4}$", id = @"^\d+$", httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiGet",
routeTemplate: "api/{controller}/{action}/{device_id}",
defaults: null,
constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiPost",
routeTemplate: "api/{controller}/{action}/{device_id}",
defaults: null,
constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
);
And added [HttpGet]
to all my GetOne and GetAll methods.
With the following results:
{host}/api/products/all/{device_id}/07-08-2014 // works
{host}/api/products/one/{device_id}/5/07-08-2014 // works (NOTE: date and id are switched now, but I don't mind this)
{host}/api/users/all/{device_id} // works
{host}/api/users/one/{device_id}/5 // doesn't work ("Server Error in '/' Application. The resource cannot be found.")
{host}/api/users/one/{device_id}/5/00-00-0000 // works (but I want it to work without the date as well..)
{host}/api/test/enable/{device_id} // works
// Haven't tested the HttpPOSTs yet
So, now almost everything works, except for {host}/api/users/one/{device_id}/{id}
(and also with tags instead of users). It looks like the defaults: new { date = RouteParameter.Optional },
isn't working in the second (and first) MapHttpRoute.
If I can't find any solution I just stick with this one and just provide /00-00-0000
when I want a single User/Tag.. >.> I have a deadline and it already took me way, way longer than expected to simply add the date to the URL-Route..
EDIT 4b:
If I remove the [ActionName("one")]
and [ActionName("all")]
from my methods, it seems that {host}/api/users/all/{device_id}
isn't working either ("No action "all" was found on the controller 'Users' that matches the request."
), so that means when I keep the ActionNames it uses the third MapHttpRoute instead of the first for the GetAllUsers-method.. The date = RouteParameter.Optional
isn't working at all when I provide a constraint for it..
Your date constraint is incorrect - you missed backslash before "any digit" specifier \d
.
Try this constraint:
date = @"^\d{2}-\d{2}-\d{4}$"
EDIT: It seems that you are right that constraints break optional parameters. Check this post for same issue.
What i ended up doing is adding possibility of empty string to regexs. You can do that by specifying ^$
separated from rest of regex using |
.
Also I've added action="actionname"
to defaults and made date
parameter nullable.
Route config:
config.Routes.MapHttpRoute(
name: "ApiAll",
routeTemplate: "api/{controller}/all/{device_id}/{date}",
defaults: new { date = RouteParameter.Optional, action="all"},
constraints: new { date = @"^$|^\d{2}-\d{2}-\d{4}$", httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiOne",
routeTemplate: "api/{controller}/one/{device_id}/{id}/{date}",
defaults: new { date = RouteParameter.Optional, action = "one" },
constraints: new { date = @"^$|^\d{2}-\d{2}-\d{4}$", id = @"^$|^\d+$", httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiGet",
routeTemplate: "api/{controller}/{action}/{device_id}",
defaults: null,
constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) }
);
config.Routes.MapHttpRoute(
name: "ApiPost",
routeTemplate: "api/{controller}/{action}/{device_id}",
defaults: null,
constraints: new { httpMethod = new HttpMethodConstraint(HttpMethod.Post) }
);
Controller action methods (code is based on ootb values controller):
[ActionName("all")]
[HttpGet]
public IEnumerable<string> Get(string device_id, DateTime? date = null)
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[ActionName("one")]
[HttpGet]
public string Get(string device_id, int id, DateTime? date = null)
{
return "value";
}