We all know Web API 2 routing conventions and how we can use the following to automatically map onto ApiController types and actions:
config.Routes.MapHttpRoute(
"Default",
"api/{controller}/{action}"
);
public class AbcdefghijklmnoController : ApiController
{
public int Test()
{
return 5;
}
}
This works as expected, I get 5 when calling api/Abcdefghijklmno/test. But now I want to create Controller types that don't explicitely end with Controller in the end, combined with a custom Controller discovery mechanism as outlined in http://www.strathweb.com/2013/08/customizing-controller-discovery-in-asp-net-web-api/.
public class Abcdefghijklmno : ApiController
{
public int Test()
{
return 5;
}
}
The crazy thing is now that I can get the value 5 from route api/abcde/test, so what the convention based routing is really doing is simply chopping off the length of the string "Controller" from the type's name. Apart from this extremely naive implementation from Web API 2, is there any way to circumvent this?
Related
I'm struggling with this for some time now. I've searched all over the internet, didn't find any solution.
I'd like to create a webapi project with somewhat custom routing. Using VS 2019, project is of type ASP.NET WebApi on .NET Core 2.2. Routing should be like this:
Basic application must reside on url similar to "https://my.server.com/myapi". URLs which will be called are in form "https://my.server.com/myapi/{InstanceName}/{CommandName}?{customParams}"
I have one controller defined in my project and I would like to redirect all requests to that controller, where instanceName could be parameter of all the methods contained in a controller, so I would get a value for that parameter. CommandName is basicly the same as "action" RouteData by MVC principles. As you can see there is no controller specified, since all is handled by one controller.
So far I've tried setup routing like this:
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "MyRoute",
template: "{instance}/{action}",
defaults: new { controller = "MyController" });
});
}
MyController.cs
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, InfoCommand model)
{
// Just return serialized model for now.
var result = new JsonResult(model);
return result;
}
}
But this does not work. I get 415 response from (I think) web server when I call for example
https://my.server.com/myapi/MYINSTANCE/info?param1=value1¶m2=value2
While debugging from VS this URL looks like this:
https://localhost:12345/MYINSTANCE/info?param1=value1¶m2=value2
but I think it shouldn't matter for routing.
In best case scenario (putting [Route("{instance}")] above controller and [HttpGet("info")] above Info method) I get 404 response, which is also what I do not want.
I've even tried creating my own ControllerFactory, but that didn't work either (changing controller inside ControllerFactory's create method and adding another parameter to RouteData).
How to setup routing like that? Is it even possible? I would still like to use all other MVC features (model binding, proper routing, auth features, etc.), it's just this routing I cannot figure it out.
Your attempt resulting a in 415 Unsupported Media Type error was your best one.
You were only missing the FromQuery as shown below.
The error indicates that the complex type InfoCommand could not be resolved.
You must specify that it must be parsed from the querystring.
Note that the route defined via MapRoute doesn't have effect, since you are using attribute-based routing; it's only one or the other.
[Route("/")]
[ApiController]
public class MyController : ControllerBase
{
[HttpGet("{instance}/info")]
public JsonResult Info(string instance, [FromQuery] InfoCommand model)
{
var result = new JsonResult(model);
return result;
}
}
public class InfoCommand
{
public InfoCommand()
{}
public string Param1 { get; set; }
public string Param2 { get; set; }
}
This is my api configuration class:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
}
}
This is my api controller class:
public class RoleController : ApiController
{
// Some action that works fine...
// Another action that works fine...
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
I am calling my actions using POST and they are working fine.
But, when I try to call the Delete action using POST I get the following error:
405 Method Not Allowed
The requested resource does not support http method 'POST'.
Clearly, this is because ApiController enforces REST convention
which expects DELETE verb for Delete action.
Now, how do I disable this REST convention constraints
and write my actions in a classic manner?
You can use the HttpPostAttribute to enforce the Action to accept only POST:
public class RoleController : ApiController
{
[HttpPost]
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
You may want to keep the REST conventions while allowing certain clients (like HTML forms) to properly use you actions.
So, you can use a combination of HttpPostAttribute and HttpDeleteAttribute or AcceptVerbsAttribute (which allows multiple verbs) to allow multiple verbs:
public class RoleController : ApiController
{
[HttpPost, HttpDelete]
// OR
[AcceptVerbs("DELETE", "POST")
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
If you don't want magic verbs and magic action names you can use route attributes.
Delete config.Routes.MapHttpRoute and set:
config.MapHttpAttributeRoutes();
Now you have to set the routes manually:
[RoutePrefix("~/Role")]
public class RoleController : ApiController
{
[HttpPost]
[Route("~/Delete")]
public Result Delete([FromBody]int RoleID)
{
return RoleBL.Delete(RoleID);
}
}
In your case I'd stop using any kind of REST conventions.
Instead of having a Delete method on the Role controller you can have a DeleteRole method and allow POST on it. This way nothing will interfere with what you want to do. Nothing forces you to build a REST api if that's not what you need.
There are several things you could do to still build a nice api.
For example, you could return an IHttpActionResult
your method could look like this:
public class RoleController : ApiController
{
[HttpPost]
public IHttpActionResult DeleteRole([FromBody]int RoleID)
{
if ( RoleID <= 0 )
{
return BadRequest();
}
var deleteResult = RoleBL.Delete(RoleID);
return Ok(deleteResult);
}
}
You still return the same object but it's wrapped inside an object with a proper http code so your code which deals with the result, won't change much.
I'm pretty new to setting up routes and routing in MVC. At my last job we used attribute routing for our WebAPI stuff, so I'm pretty familiar with that (RoutePrefix, Route, and HttpGet/HttpPost attributes, etc). And I can get my current project to work just fine with attributes.
So now what I want to do is "prefix" all of the webApi routes with "api". So instead of going to mysite/test/hello, I want to go to mysite/api/test/hello.
This is what I have, using only attribute routing, and it works just fine:
[RoutePrefix("Test")]
public class TestController : ApiController
{ ....
[HttpPost]
[Route("{message}")]
public HttpResponse EchoBack(string message)
{
// return message ... in this case, "hello"
}
}
Now, I know I can change the RoutePrefix to "api/Test" (which works 100%), but I don't want to change all my controllers, I would rather be able to perform this by changing the values passed in to config.Routes.MapHttpRoute in WebApiConfig.
Is this possible?
What you describe can be done in attribute routing by using what is referred to as a global route prefix.
Referencing this article Global route prefixes with attribute routing in ASP.NET Web API
Create a DirectRouteProvider
public class CentralizedPrefixProvider : DefaultDirectRouteProvider {
private readonly string _centralizedPrefix;
public CentralizedPrefixProvider(string centralizedPrefix) {
_centralizedPrefix = centralizedPrefix;
}
protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor) {
var existingPrefix = base.GetRoutePrefix(controllerDescriptor);
if (existingPrefix == null) return _centralizedPrefix;
return string.Format("{0}/{1}", _centralizedPrefix, existingPrefix);
}
}
To quote article.
The CentralizedPrefixProvider shown above, takes a prefix that is
globally prepended to every route. If a particular controller has it’s
own route prefix (obtained via the base.GetRoutePrefix method
invocation), then the centralized prefix is simply prepended to that
one too.
Configure it in the WebAPiConfig like this
config.MapHttpAttributeRoutes(new CentralizedPrefixProvider("api"));
So now for example if you have a controller like this
[RoutePrefix("Test")]
public class TestController : ApiController {
[HttpPost]
[Route("{message}")]
public IHttpActionResult EchoBack(string message) { ... }
}
The above action will be accessed via
<<host>>/api/Test/{message}
where api will be prepended to the controller actions route.
I'm trying to create basic REST api with a base controller like so:
Base class:
public abstract class WebApiEntityController<TEntity> : ApiController
where TEntity : EntityBase<TEntity, int>
{
private readonly IRepository<TEntity> _repository;
protected WebApiEntityController(IRepository<TEntity> repository)
{
_repository = repository;
}
[Route("")]
[WebApiUnitOfWork]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK, _repository.ToList());
}
[..........]
Derived class:
[RoutePrefix("api/TimesheetTask")]
public class TimesheetTaskController : WebApiEntityController<TimesheetTask>
{
private readonly IRepository<TimesheetTask> _timeSheetTaskRepository;
public TimesheetTaskController(IRepository<TimesheetTask> timeSheetTaskRepository) : base(timeSheetTaskRepository)
{
_timeSheetTaskRepository = timeSheetTaskRepository;
}
}
but calling GET on the route ~/api/TimesheetTask/ results in a 404 not found.
According to this answer, attribute routes cannot be inherited. So my question is, how can I write a consistent API for all my domain models without having to copy and paste code?
I know I can do convention routing with this configuration:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
but then I'll have to specify the action, and my endpoints will be
/api/{controller]/Get
/api/{controller]/Post
and I don't want that. I can also remove the {action} part of the routeTemplate, but then how will I route to custom actions?
If anyone can help, that would be appreciated. Also, the next step for my domain model API would involve supporting querying, and that can easily get complicated. Is there a library that generates these routes for you? If anyone can help me find such a library it would be much appreciated.
The answer you quoted has since been updated. As of WebApi 2.2 they created an extensibility point to allow the feature you wanted.
Attribute rout can be inherited, but you need to configure it. I had the same requirement for a base API controller and after searching came across the same answer you quoted.
.NET WebAPI Attribute Routing and inheritance
You need to overwrite the DefaultDirectRoutePrivider:
public class WebApiCustomDirectRouteProvider : DefaultDirectRouteProvider {
protected override System.Collections.Generic.IReadOnlyList<IDirectRouteFactory>
GetActionRouteFactories(System.Web.Http.Controllers.HttpActionDescriptor actionDescriptor) {
// inherit route attributes decorated on base class controller's actions
return actionDescriptor.GetCustomAttributes<IDirectRouteFactory>(inherit: true);
}
}
With that done you then need to configure it in your web api configuration
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
.....
// Attribute routing. (with inheritance)
config.MapHttpAttributeRoutes(new WebApiCustomDirectRouteProvider());
....
}
}
I have a scenario where I have 2 different controllers that each have multiple Get methods. I have the methods decorated with the ActionName attribute, but the Routing isn't working as I think it should.
ContactController
public ContactModel GetContactByID(string id)
{
...
}
[ActionName("username")]
public ContactModel GetContactByUserName(string text)
{
...
}
PaymentController
public PaymentModel Get(Guid id)
{
...
}
[HttpGet, ActionName("sale")]
public PaymentActivityModel Sale(Guid id)
{
...
}
Routes
config.Routes.MapHttpRoute(
"PaymentControllerActionId",
"api/client/{clientId}/{controller}/{action}/{id}",
defaults: null
);
config.Routes.MapHttpRoute(
"ContactControllerActionText",
"api/client/{clientId}/{controller}/{action}/{text}",
defaults: null
);
config.Routes.MapHttpRoute(
"ClientControllerId",
"api/client/{clientId}/{controller}/{id}",
new { id = RouteParameter.Optional }
);
When I navigate to a Payment, it works fine. But when I navigate to a Contact, I receive:
No HTTP resource was found that matches the request URI, .../api/client/.../contact/username/exampleUserName
No action was found on the controller 'Contact' that matches the request.
I was under the impression that the parameter name would be matched up with the action name (text vs. id).
Is the problem that one uses a Guid id and one uses a string id?
This is a client-facing API, and I have a client-friendly ContactID that is a string. Whereas the PaymentID is a Guid.
First I want to point out your routes don't make a lot of sense.
This route says it is for PaymentControllerActionId, however you left the routing open to use any controller instead of specifying it is only for the Payment Controller.
config.Routes.MapHttpRoute(
"PaymentControllerActionId",
"api/client/{clientId}/{controller}/{action}/{id}",
defaults: null
);
To answer your question. You likely need to specify {text} as an optional RouteParameter. Otherwise the framework usually expects that you have defined a custom routing constraint (and you should have a routing constraint on {clientId} IMO).
Also, if one of your actions expects a GUID. Put it as a Guid argument type, and WebAPI will match it correctly. Alternatively, since both functions are expecting either a name or a guid, you could just have only 1 function that takes a string, and in your db do a select statement matching the GUID or the NAME and return the contact. Much less code, easier to read, makes more sense.
If you are using Web API 2, then you could use attribute routing for this purpose.
In the below example, the controllers use a mix of conventional + attribute routing, but you can change this to only go by attribute routing if you need.
Example:
Route Configuration
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
"ClientControllerId",
"api/client/{clientId}/{controller}/{id}",
new { id = RouteParameter.Optional }
);
ContactController
[RoutePrefix("api/client/{clientId}/Contact")]
public class ContactController
{
// this uses conventional route
public ContactModel GetContactByID(string id)
{
...
}
[Route("username/{userName}")]
public ContactModel GetContactByUserName(string userName)
{
...
}
}
PaymentController
[RoutePrefix("api/client/{clientId}/Payment")]
public class PaymentController
{
// this uses conventional route
public PaymentModel Get(Guid id)
{
...
}
[HttpGet, Route("sale/{id}")]
public PaymentActivityModel Sale(Guid id)
{
...
}
}