I'm transitioning out of using WCF Data Services (since they're apparently dead), and attempting to build OData feeds using the newer Microsoft ASP.NET WebApi 2.1. I'm looking for a way to rename my feeds to be different than the class name.
I built out all my controllers, and now I'm trying to rename them just slightly to preserve the idea of set vs. single entities. (For example, the feed should be named WorkCategories, but the class name should be WorkCategory). Is this possible? I want to do something like this:
public static void Register(HttpConfiguration config)
{
builder.EntitySet<EmailSequenceItem>("EmailSequenceItems");
builder.EntitySet<EmailSequence>("EmailSequences");
builder.EntitySet<WorkCategory>("WorkCategories");
...
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
config.MapHttpAttributeRoutes();
}
My controller looks like this (built from templates):
public class WorkCategoryController: ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategory()
{
return db.WorkCategories;
}
...
}
But what I get when I rename any of the feeds is a 404 when navigating to http://localhost/odata/WorkCategories:
HTTP/1.1 404 Not Found
Message: No HTTP resource was found that matches the request URI 'http://localhost/odata/WorkCategories'."
MessageDetail: No type was found that matches the controller named 'WorkCategories'
The name of the controller should be same with the EntitySet name by default, which is WorkCategories.
So other controller name except for WorkCategoriesController won't work unless you create your own IODataRoutingConvention.
For the method name, webapi has its default routing rule.
For get entityset, GetWorkCategories() and Get() will work.
For get entity, GetWorkCategory(int key) and Get(int key) will work.
If you want to customize the method name, you can use AttributeRouting in webapi 2.2.
http://blogs.msdn.com/b/webdev/archive/2014/03/13/getting-started-with-asp-net-web-api-2-2-for-odata-v4-0.aspx
[ODataRoute("WorkCategories/WhateverName")]
public IQueryable WhateverName() {...}
Aha! I found this just after I posted it. I just need to rename my controller class to WorkCategoriesController, and the 2 Queryable methods to GetWorkCategories instead of WorkCategory
public class WorkCategoriesController : ODataController
{
private dcMaintContext db = new dcMaintContext();
// GET odata/WorkCategory
[Queryable]
public IQueryable<WorkCategory> GetWorkCategories()
{
return db.WorkCategories;
}
// GET odata/WorkCategory(5)
[Queryable]
public SingleResult<WorkCategory> GetWorkCategories([FromODataUri] int key)
{
return SingleResult.Create(db.WorkCategories.Where(workcategory => workcategory.ID == key));
}
...
}
Related
I use OpenAPI (Swagger) in a .NET Core project and when using multiple methods that have similar get requests, I encounter "Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints." error during runtime. I look at several pages on the web and SO and tried to apply the workarounds like The request matched multiple endpoints but why?, but it does not fix the problem. Here are the API methods and route definitions that I use.
[Route("get", Name="get")]
public IEnumerable<DemoDto> Get()
{
//
}
[Route("get/{id}", Name="getById")]
public DemoDto GetById(int id)
{
//
}
[Route("get/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination(DemoQuery query)
{
//
}
I use Name property in order to fix the problem but not solved. Any idea to make changes on the routes to differentiate Get() and GetWithPagination()?
You have two endpoints with equals routes:
get/{id} and get/{query}.
If you write in browser line: get/123, the system can't understand what route to use, because they have the same pattern.
You need to distinguish them and I suggest you use restful style for routes, like:
item/{id},
items?{your query}
[Route("get/{query}", Name="getWithPagination")]
This doesn't make sense. DemoQuery is an object, it can't be represented by a single part of a url. You can tell the ModelBinder to build your object from multiple query parameters, though.
The routing engine is getting this route confused with the [Route("get/{id}", Name="getById")] route. They both appear to match get/blah.
In addition to fixing your DemoQuery route, try adding a route constraint on the id route -
[Route("get/{id:int}", Name="getById")]
to better help the engine.
To get DemoQuery to work, assume it looks something like:
public class DemoQuery
{
public string Name { get; set; }
public int Value { get; set; }
}
Then change your action to
[Route("getPaged/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination([FromQuery] DemoQuery query)
and call then endpoint like /getPaged?name=test&value=123. And the ModelBinder should build your object for you.
ASP.NET Web API 2 supports a new type of routing. Offical Doc
Route constraints let you restrict your parameters type and matched with these types (int, string, even date etc). The general syntax is "{parameter:constraint}"
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
I tested at API;
//match : api/users/1
[HttpGet("{id:int}")]
public IActionResult GetUserById(int id){ ... }
//match : api/users/gokhan
[HttpGet("{name}")]
public IActionResult GetUserByName(string name){ ... }
I'd like to have a catch all route that matches a general route prefix ("api/myaccount/1") execute if there are no more specific routes defined on other controllers (i.e "api/myaccount/1/feature") however I get the following exception when I do this:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.
As mentioned here:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL it seems this may not be possible.
Wanting a default route to execute when no better one is found sounds pretty common so what am I missing? Do I need to hook lower in the pipeline or something...
FYI: I have the catch all working fine ("api/myaccount/1/{*uri}") it's just the being able to override it that's the problem.
Turns out this is pretty easy, I just needed to create a custom Controller Selector and override the GetControllerName function. That particular override is required because the method you would expect to override:
HttpControllerDescriptor SelectController(HttpRequestMessage request)
does not just return the descriptor (or null if It can't find a match) as you may expect. The method actually handles the request for you and returns a 404 :/ However once you're aware of that it is trivial to work around and I was able to get the behavior I wanted using the code below:
using System.Web.Http;
using System.Web.Http.Dispatcher;
public class CustomControllerSelector : DefaultHttpControllerSelector
{
public override string GetControllerName(HttpRequestMessage request)
{
var name = base.GetControllerName(request);
if(string.IsNullOrEmpty(name))
{
return "MyFeature"; //important not to include "Controller" suffix
}
return name;
}
}
And add it to your configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
...
}
}
I am trying to add a Get() function in a MVC 6 (Asp .Net 5) Web Api to pass a configuration option as a query string. Here are the two functions that I already have:
[HttpGet]
public IEnumerable<Project> GetAll()
{
//This is called by http://localhost:53700/api/Project
}
[HttpGet("{id}")]
public Project Get(int id)
{
//This is called by http://localhost:53700/api/Project/4
}
[HttpGet()]
public dynamic Get([FromQuery] string withUser)
{
//This doesn't work with http://localhost:53700/api/Project?withUser=true
//Call routes to first function 'public IEnumerable<Project> GetAll()
}
I've tried several different ways to configure the routing, but MVC 6 is light on documentation. What I really need is a way to pass some configuration options to the list of Projects for sorting, custom filtering etc.
You can't have two [HttpGet]s with the same template in a single controller. I'm using asp.net5-beta7 and in my case it even throws the following exception:
Microsoft.AspNet.Mvc.AmbiguousActionException
Multiple actions matched. The following actions matched route data and had all constraints satisfied:
The reason for this is that [From*] attributes are meant for binding, not routing.
The following code should work for you:
[HttpGet]
public dynamic Get([FromQuery] string withUser)
{
if (string.IsNullOrEmpty(withUser))
{
return new string[] { "project1", "project2" };
}
else
{
return "hello " + withUser;
}
}
Also consider using Microsoft.AspNet.Routing.IRouteBuilder.MapRoute() instead of attribute routing. It may give you more freedom defining the routes.
Accessing the query string is very doable by using either the RESTful routing conventions (enforced by ASP.NET 5 / MVC 6 by default) or by defining custom routes, as explained in this answer.
Here's quick example using custom, attribute-based routes:
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
For more info about custom routing, read the following article on my blog.
I just have one quick question about what seems to have been a limitation with ASP.NET Web API Attribute Routing, but hoping I just suck at research. In my controller, I'm trying to do something like this:
public class OrdersController : ApiController
{
[HttpGet]
[Route("{apiRoot}/customers/{id:int}/orders")]
public IHttpActionResult GetCustomerOrders(int id) {...}
}
Where {apiRoot} is defined in either a configuration file.
This may not actually be necessary, but I'd like to know how to put a specific path in the route attribute without having to code a static path. Is the general idea here supposed to be that you only put text into the route path, except for your parameters which go in {}?
How about switching to using a RoutePrefix:
[MyRoutePrefix]
public class OrdersController : ApiController
{
[HttpGet]
[Route("customers/{id:int}/orders")]
public IHttpActionResult GetCustomerOrders(int id) {...}
}
public class MyRoutePrefixAttribute : RoutePrefixAttribute
{
public MyRoutePrefixAttribute()
{
Prefix = "the route prefix";
}
}
RoutePrefixAttribute isn't sealed like RouteAttribute so extending it should allow you do what you need. Assuming, of course, that all of the controllers in a single class using the same root path.
Note: I haven't had a chance to try this but given what I know of attribute routing, I don't see why it shouldn't work.
I've recently asked a few questions about the best way to create a web api which utilises the same url as my main mvc site. I deduced the best way was to make the necessary changes to my MVC site to include web api and the necessary routing.
I have mainly followed How to add Web API to an existing ASP.NET MVC 4 Web Application project? but I have run into problems. The code compiles fine and it is clearly looking for the route but I get the error:
No HTTP resource was found that matches the request URI 'http://localhost:2242/api/value'.
No type was found that matches the controller named 'value'.
My WebApiConfig:
class WebApiConfig
{
public static void Register(HttpConfiguration configuration)
{
configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional });
}
}
my global.asax:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
Database.SetInitializer<ApplicationDbContext>(null);
}
}
my api controller:
public class ValuesController1 : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
public string Get(int id)
{
return "value";
}
// POST api/<controller>
public void Post([FromBody]string value)
{
}
// PUT api/<controller>/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/<controller>/5
public void Delete(int id)
{
}
}
Other posts have corroborated that this is a correct and working setup...I created a separate webapi project to compare and this is all correct routing wise apparently. It would be far preferable to build this into my MVC website, does anyone have any ideas? This poster No type was found that matches controller had the same problem and the solution he found was to copy everything into a new project....that really isn't something I want to do/see why I should need to do.
I think it is because of your Controller's name : ValuesController1
A controller has to be suffixed by "Controller", the 1 may be the cause of your issue.
The name of the controller ValuesController1 doesn't match convention - in order for the default route to match /api/value based on the default convention set in your call to configuration.Routes.MapHttpRoute(...), the controller should be called ValueController:
public class ValueController : ApiController
{
public IEnumerable<string> Get()
// ...
However, if you intend to deviate from the configured convention, you can apply RouteAttribute and RoutePrefixAttribute in conjunction with the Http* verb attributes to customise controller and method routes, e.g.
[RoutePrefix("api/Foo")]
public class ValuesController : ApiController
{
// get api/Foo/value
[HttpGet]
[Route("value")]
public IEnumerable<string> NameDoesntMatter()
{
return new string[] { "value1", "value2" };
}
// get api/Foo/value/123
[HttpGet]
[Route("value/{id}")]
public string AnotherRandomName(int id)
{
return "value";
}
Before using the RouteAttribute you will need to add the following to your WebApiConfig.Register(HttpConfiguration config):
config.MapHttpAttributeRoutes();
Even with the routing attributes, note however that the controller class name still needs to end with the suffix Controller, i.e. cannot end in the suffix 1. It is surprisingly difficult to alter this convention.