WebApi Path differences when using RouteAttribute or implicit routing - c#

I've been working with WebApi and I'm trying to understand how the difference in using the Route Attribute vs having WebApi automatically create the route based on the function name works. I have many controllers of this form:
public class MyController : RestControllerBase
{
public async Task<HttpResponseMessage> GetData()
{
return _data;
}
[Route(ApiVersion.Version + "/MyController/{param}/specific/route")]
public async Task<HttpResponseMessage> GetMoreData()
{
return _moreData;
}
}
This controller and others all specify a route registry that does:
routingManager.RegisterVersionedHttpRoute(
routeName: "MyController",
routeTemplate: "MyController/{param}",
defaults: new { controller = "MyController", param = RouteParameter.Optional },
version: ApiVersion.Version);
When I build and test my API, both end points are correctly accessible.
I also overrode the HttpControllerSelector with one based on namespaces (for versioning).
My problem is that these two routes, besides the traditional function of being called by a specific http route, behave differently. For example, in my overridden controller selector I had to write this logic in order for the controllers and routes to properly map:
private string GetControllerName(IHttpRouteData routeData)
{
if (routeData.GetSubRoutes() != null)
{
// With route attribute
var subroute = routeData.GetSubRoutes().FirstOrDefault();
var dataTokenValue = subroute.Route.DataTokens["actions"];
if (dataTokenValue == null) return null;
var controllername = ((HttpActionDescriptor[])dataTokenValue).First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
return controllername;
}
else
{
// Try Strategy for without route attribute.
return (String)routeData.Values["controller"];
}
}
private string GetVersion(IHttpRouteData routeData)
{
var routeTemplate = "";
if (routeData.GetSubRoutes() != null)
{
var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
if (subRouteData == null) return null;
routeTemplate = subRouteData.Route.RouteTemplate;
}
else
{
routeTemplate = routeData.Route.RouteTemplate;
}
var index = routeTemplate.IndexOf("/");
if (index == -1)
{
return Unversioned;
}
var version = routeTemplate.Substring(0, index).ToUpperInvariant();
return version;
}
It seems as though routes with the explicit attribute will have a route.GetSubroutes() available, whereas the implicit routes will not.
This causes me to write two different approaches to extracting the version and controller names from the incoming request before I can route them properly. I can live with that, but it really bothers me that something internal to WebApi is creating this distinction between the routes.
Unfortunately, I then started working with https://github.com/domaindrivendev/Swashbuckle to document my api and ran into similar issues. The swagger page generates correctly for routes that have the Route attribute, but completely ignores the implicit routes. I suspect the problem causing this issue is the same that causes the branch in my selecting logic.
I've been digging through as much ApiExplorer, WebAPI and other documentation as I can find online, but I haven't been able to find the solution to homogenize the implicit/explicit route approaches. It may even be a bug in their implementation.
Where should I go from here?

So the answer here is that there is a bug or inconsistency with Web Api 2
To verify, you can run this code snippet. In the case I mentioned, ApiExplorer does not return results for paths that are not explicitedly marked with a route attribute.
IApiExplorer apiExplorer = configuration.Services.GetApiExplorer();
foreach (ApiDescription api in apiExplorer.ApiDescriptions)
{
Console.WriteLine("Uri path: {0}", api.RelativePath);
Console.WriteLine("HTTP method: {0}", api.HttpMethod);
foreach (ApiParameterDescription parameter in api.ParameterDescriptions)
{
Console.WriteLine("Parameter: {0} - {1}", parameter.Name, parameter.Source);
}
Console.WriteLine();
}

Related

Web API: A route named 'X' could not be found in the route collection

I have tried all solutions found both here on Stackoverflow and elsewhere on the internet with regards to this error and still we are getting an issue.
So we have .NET API which has a POST method which then returns a CreatedAtRoute response (201). The problem is that when returning the CreatedAtRoute response we get the error "A route named 'X' could not be found in the route collection.", where X is the name of our route.
The Global.asax
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
GlobalConfiguration.Configuration.UseStructureMap<MasterRegistry>();
var allDirectRoutes = WebApiConfig.GlobalObservableDirectRouteProvider.DirectRoutes;
}
WebApi.config - We have the MapHttpAttributes declared before the default route.
public static class WebApiConfig
{
public static ObservableDirectRouteProvider GlobalObservableDirectRouteProvider = new ObservableDirectRouteProvider();
public static void Register(HttpConfiguration config)
{
config.Formatters.Clear();
config.Formatters.Add(new JsonMediaTypeFormatter());
// Web API routes
config.MapHttpAttributeRoutes(GlobalObservableDirectRouteProvider);
config.Routes.MapHttpRoute(
"DefaultApi",
"api/v1/{controller}/{id}",
new { id = RouteParameter.Optional }
);
}
}
Controller - GetCompoundById route
This is the route we want to build using the named route
[HttpGet]
[Route("{id:Guid}", Name = "GetCompoundById")]
[SwaggerResponse(HttpStatusCode.OK, Type = typeof(CompoundViewModel))]
public async Task<IHttpActionResult> Get(Guid id)
{
var serviceResult = await Task.FromResult(CompoundService.Get(id));
if (serviceResult == null)
{
return NotFound();
}
CompoundViewModel result =
new CompoundViewModel {Id = serviceResult.Id, Name = serviceResult.Name};
return Ok(result);
}
Controller - Return CreatedAtRoute in the POST action
This is where the error is thrown because the named route is not found.
return CreatedAtRoute("GetCompoundById", new {id = result.Id}, result);
Note: In the WebApi.config I have created an ObservableDirectRouteProvider which allows me to see the routes created on startup and I can see my named route exists in the collection.
If we are using route prefix at controller, we should define route name for all actions. Use this
[HttpGet]
[Route(Name = "GetCompounds")]
[SwaggerResponse(HttpStatusCode.OK, Type = typeof(IEnumerable<ApiModels.CompoundViewModel>))]
public async Task<IHttpActionResult> Get(int page = 0,int pageSize = CoreModels.Pagination.DefaultPageSize)
The issue was strangely nothing to do directly with the way we were using the named routes or configuring WebAPI routing (which explains why all other posts didn't help me fix it). We found that the issue was a side effect of how we were using StructureMap as our IoC container.
In the StructureMap registry we were calling
scanner.SingleImplementationsOfInterface();
which had the strange side-effect of causing the error. By performing a very long and tedious process of elimination I tracked it down to this exact line. Once remove the routing then again worked as expected. I can only presume that this causes some WebApi dependency to load the incorrect type into memory which cannot resolve the routing table.

Nopcommerce RegisterRoutes

Hi I have problem with routes in plugin, in nopcommerce 3.6
I have in folder Controller TestPohodaController.cs contains method ImportProductInfo()
There is my RegisterRoutes:
namespace Nop.Plugin.Test.Pohoda
{
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Plugin.Test.Pohoda.ImportProductInfo",
"Plugins/TestPohoda/ImportProductInfo",
new { controller = "TestPohoda", action = "ImportProductInfo" },
new[] { "Nop.Plugin.Test.Pohoda.Controllers" }
);
}
public int Priority
{
get
{
return 0;
}
}
}
}
Installation to nopCommerce is ok, but when I go to mypage/Plugins/TestPohoda/ImportProductInfo page return 404.
I need url of TestPohodaController to call this controller from economic system. Can You help me please? Thanks.
ASP.NET MVC Routing evaluates routes from top to bottom. So if two routes match, the first one it hits (the one closer to the 'top' of the RegisterRoutes method) will take precedence over the subsequent one.
With that in mind, you need to do two things to fix your problem:
Your default route should be at the bottom.
Your routes need to have constraints on them if they contain the same number of segments:
What's the difference between:
example.com/1
and
example.com/index
To the parser, they contain the same number of segments, and there's no differentiator, so it's going to hit the first route in the list that matches.
To fix that, you should make sure the routes that use ProductIds take constraints:
routes.MapRoute(
"TestRoute",
"{id}",
new { controller = "Product", action = "Index3", id = UrlParameter.Optional },
new { id = #"\d+" } //one or more digits only, no alphabetical characters
);
You don't need to start with Plugins for your route url. it is enough
to follow this pattern {controller}/{Action}/{parameter}
Make sure also namespace for the controller is correct as you define
in the routing. Nop.Plugin.Test.Pohoda.Controllers
You can define an optional productId parameter as well. so it will
work for mypage/TestPohoda/ImportProductInfo or
mypage/TestPohoda/ImportProductInfo/123
You can also set the priority higher than 0 which is priority of the
default routeprovider in the nop.web. this way you ensure that your
plugin reads it first. Indeed it is not necessary as you have
specific url. this is only required if you have similar route url
Try using this route
namespace Nop.Plugin.Test.Pohoda
{
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Plugin.Test.Pohoda.ImportProductInfo",
"TestPohoda/ImportProductInfo/{productId}",
new { controller = "TestPohoda", action = "ImportProductInfo" , productId = = UrlParameter.Optional },
new[] { "Nop.Plugin.Test.Pohoda.Controllers" }
);
}
public int Priority
{
get
{
return 1;
}
}
}
}
We will have a look at how to register plugin routes. ASP.NET routing is responsible for mapping incoming browser requests to particular MVC controller actions. You can find more information about routing here. So follow the next steps:
If you need to add some custom route, then create RouteProvider.cs file. It informs the nopCommerce system about plugin routes. For example, the following RouteProvider class adds a new route which can be accessed by opening your web browser and navigating to http://www.yourStore.com/Plugins/PaymentPayPalStandard/PDTHandler URL (used by PayPal plugin):
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute("Plugin.Payments.PayPalStandard.PDTHandler", "Plugins/PaymentPayPalStandard/PDTHandler",
new { controller = "PaymentPayPalStandard", action = "PDTHandler" });
}
public int Priority
{
get
{
return -1;
}
}
}
It could be cache problem, try to restart IIS
actually you do nota have to register route by default you can call your method
/TestPohoda/ImportProductInfo

Routing Controllers asp.net mvc

In our application users can create objects and give them a name. This data is stored in the database with a typeId. I have an api controller that can currently be called, with a name like
api/data
This controller has the basic Get, Put, Post and Delete methods.
Now i would like users to be able to call this controller by object type. So if they setup 3 different objects and call them company, contact and project, i would like them to be able to call the api/data controller, using these names, similar to this
api/company
api/contact
api/project
I do not know these object types unitl runtime, so i cannot code these manually.
Now i also have other controllers that i do not want effected by this, so if i have a normal controller called page, then i still want to be able to call it by
api/page
Is there a way i can do this? Almost intercept a call to a controller, see if the name is equal to a name i have in the database, and if so pass it to the data controller, else let it process as normal.
As far as you are using api controller, you may play with RoutePrefix and Route annotations. For example:
[RoutePrefix("api")]
public class DataController : ApiController
{
[Route("company")]
public IEnumerable<CompanyViewModel> GetCompanies()
{
....
}
}
But this may conflict with default route definition. If so, try to specifically define your routes for this controller:
config.Routes.MapHttpRoute(
name: "CompanyApi",
routeTemplate: "api/company/{id}",
defaults: new {controller = "DataController" , id = RouteParameter.Optional}
);
You would need to add a route for each object in your db, or rather each object not from the db, and have all others route to a default path. You may want to look at using regex with your routing, if you are able to prefix the object names at all.
I think you should
HomeController:
ActionResult Index(string name)
{
// check if name exists in DB
}
I didn't see other way to do what you want even by editing routes... or maybe if you do routes with DB but well...
Web API routing uses fall-through logic, so start off by declaring your known routes first, then have a generic handler to catch all other requests and handle them accordingly. For example, use the following code when setting up your HttpConfiguration:
// Define all known routes using attributes.
config.MapHttpAttributeRoutes();
// Generic route to handle all other requests
config.Routes.MapHttpRoute(
name: "DynamicRoute",
routeTemplate: "api/{dataType}",
defaults: new { controller = "data" }
);
Then in your DataController you can have actions that take the dataType as a parameter:
public async Task<IHttpActionResult> Get(string dataType, ...)
{ ... }
I have managed to do what i want by using RoutePrefix attribute on controller with an extra MapRoute, with a constraint applied to it.
Here is the code, if someone else is trying to do something similar
First add a routePrefix to the controller that you want to call
[RoutePrefix("api/{myCustomName}")]
public MyController : ApiController {
}
Now in my WebApiConfig file, i needed to add the following lines
// Web API routes
config.MapHttpAttributeRoutes();
// handle calling MyController using custom name
config.Routes.MapHttpRoute(
name: "ModuleDataApi",
routeTemplate: "api/{myCustomName}/{id}",
defaults: new { controller = "MyController" },
constraints: new { moduleType = new MyCustomRouteConstraint() }
);
Now create the constraint that checks if there is a valid match. In my code, i have populated a text file with the details if the file doesnt exist, and i will delete/refresh this when a new custom name is added. I believe this would perform better then a database query everytime, since i would probably have about 20 name/value pairs in here at the very most.
So create your constraint like so
public class MyCustomRouteConstraint : IHttpRouteConstraint
{
private DbContext _context;
private static object fileLockObject = new object();
public MyCustomRouteConstraint ()
{
_context = new DbContext();
}
public bool Match(System.Net.Http.HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
var appDataPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/CustomNameRouteCache.txt");
// lock variable so no 2 or more threads can try to create file at once
lock (fileLockObject)
{
var list = new List<MyRouteModel>();
// check if our cache file exists, if not, lets create it
if (!File.Exists(appDataPath))
{
using (var writer = new System.IO.StreamWriter(appDataPath))
{
// get all modules
var names = _context.CustomNames.ToList();
foreach (var item in names)
{
list.Add(Mapper.Map<MyRouteModel>(item));
}
// serialize the list to the file in json format
var json = JsonConvert.SerializeObject(list);
// write to file
writer.Write(json);
};
}
else
{
// open file and get json contents
using (var reader = new System.IO.StreamReader(appDataPath))
{
var json = reader.ReadToEnd();
list = JsonConvert.DeserializeObject<List<MyRouteModel>>(json);
}
}
// lets search our list and see if this value is in our modules
foreach (var item in list)
{
if (item.Name.Equals(values[parameterName].ToString()))
{
return true;
}
}
}
return false;
}
}
Now this seems to work perfectly, i can call MyController using the following paths, or any others that are created by the user at runtime
api/company/
api/contact/
api/project/
api/whateveryouwant/
Hope this help someone else trying to do something similar

Attribute Routing and Query Strings

I am new to web api coming from a WCF background and as prep I watched Shawn Wildermuth's Pluralsight course on the subject before diving in. His course material was designed around more traditional routing. One of the subjects the course dives into is HATEOAS and how easy it is to achieve this with a base api controller and model factory.
One of the first things I hit when implementing against attribute routing was the need for the UrlHelper to have a route name as the first argument of the Link() method, something that was inherited in the conventional routing configured in the WebApiConfig.cs.
I worked around this by decorating one of my controllers route attributes with the Name property and it appears that all methods in that controller have access to the name property regardless of which method I put it on (see code below). While I find this a bit odd, it works. Now that I had HATEOAS implemented, I noticed the URL's it was generating were in the query string format and not "url" formatted (I know the term is wrong but bear with me). Instead of .../api/deliverables/1 I am getting .../api/deliverables?id=1.
This is "ok" but not the desired output. While I still have not figured out how to adjust the formatting the of the return value of the URL, I figured I would test the query string against my controller and found that in the query string format my controller does not work but in the "url" format it does.
I then spent an hour trying to figure out why. I have attempted different decorations (i.e. [FromUri] which from my reading should only be necessary for complex objects which default to the message body) to setting default values, constraints and making it optional (i.e. {id?}).
Below is the code in question, both for the controller, the base api controller and the model factory that makes the HATEOAS implementation possible.
The 3 questions I have are:
1) How to make the controller accept the "id" on the querystring AND in the url format (.../deliverables/1 and .../deliverables?id=1.
2) How to make the Link method of the URL helper return the value in the url format (it is currently returning it as a query string.
3) Proper way to name routes in WebAPI 2. What I am doing (assigning a name to a single method and the others appear to inherit it simply smells and I have to believe this would crumble as I actually start to implement more complex code. Is Shawn's implementation flawed in some way? I like not having to hard code a URL for test/development purposes but maybe UrlHelper is not the best way to achieve this. It seems to carry with it a lot of baggage that may not be necessary.
Controller:
[RoutePrefix("api/deliverables")]
public class DeliverablesController : BaseApiController
{
private readonly IDeliverableService _deliverableService;
private readonly IUnitOfWork _unitOfWork;
public DeliverablesController(IDeliverableService deliverableService, IUnitOfWorkAsync unitOfWork)
{
_deliverableService = deliverableService;
_unitOfWork = unitOfWork;
}
[Route("", Name = "Deliverables")]
public IHttpActionResult Get()
{
return Ok(_deliverableService.Get().Select(TheModelFactory.Create));
}
[Route("{id}")]
public IHttpActionResult Get(int id)
{
return Ok(TheModelFactory.Create(_deliverableService.Find(id)));
}
[Route("")]
public IHttpActionResult Post([FromBody]DeliverableModel model)
{
try
{
var entity = TheModelFactory.Parse(model);
if (entity == null)
{
return BadRequest("Could not parse Deliverable entry in body.");
}
_deliverableService.Insert(entity);
_unitOfWork.SaveChanges();
return Created(Request.RequestUri + "/" + entity.Id.ToString(CultureInfo.InvariantCulture),TheModelFactory.Create(entity));
}
catch (Exception exception)
{
return BadRequest(exception.Message);
}
}
}
Base API Controller:
public abstract class BaseApiController : ApiController
{
private ModelFactory _modelFactory;
protected ModelFactory TheModelFactory
{
get
{
return _modelFactory ?? (_modelFactory = new ModelFactory(Request));
}
}
}
Model Factory:
public class ModelFactory
{
private readonly UrlHelper _urlHelper;
public ModelFactory(HttpRequestMessage request)
{
_urlHelper = new UrlHelper(request);
}
public DeliverableModel Create(Deliverable deliverable)
{
return new DeliverableModel
{
Url = _urlHelper.Link("deliverables", new { id = deliverable.Id }),
Description = deliverable.Description,
Name = deliverable.Name,
Id = deliverable.Id
};
}
public Deliverable Parse(DeliverableModel model)
{
try
{
if (string.IsNullOrEmpty(model.Name))
return null;
var entity = new Deliverable
{
Name = model.Name,
Description = !string.IsNullOrEmpty(model.Description)
? model.Description
: string.Empty
};
return entity;
}
catch (Exception)
{
return null;
}
}
}
As a point of clarification, non-attribute (traditional) routing works without an issue for both the URI and query string formats:
config.Routes.MapHttpRoute(
name: "deliverables",
routeTemplate: "api/deliverables/{id}",
defaults: new { controller = "deliverables", id = RouteParameter.Optional }
);
In my opinion, this is one of the problems with Attributed routing. That's why I use it for exceptional cases only. I use route tables for the majority of routing then drop down into attributed routing for exceptional cases.
To solve this your way, have you thought about multiple routes on the Get(id)? (I don't actually think this would work, but its worth a try).

Custom method names in ASP.NET Web API

I'm converting from the WCF Web API to the new ASP.NET MVC 4 Web API. I have a UsersController, and I want to have a method named Authenticate. I see examples of how to do GetAll, GetOne, Post, and Delete, however what if I want to add extra methods into these services? For instance, my UsersService should have a method called Authenticate where they pass in a username and password, however it doesn't work.
public class UsersController : BaseApiController
{
public string GetAll()
{
return "getall!";
}
public string Get(int id)
{
return "get 1! " + id;
}
public User GetAuthenticate(string userName, string password, string applicationName)
{
LogWriter.Write(String.Format("Received authenticate request for username {0} and password {1} and application {2}",
userName, password, applicationName));
//check if valid leapfrog login.
var decodedUsername = userName.Replace("%40", "#");
var encodedPassword = password.Length > 0 ? Utility.HashString(password) : String.Empty;
var leapFrogUsers = LeapFrogUserData.FindAll(decodedUsername, encodedPassword);
if (leapFrogUsers.Count > 0)
{
return new User
{
Id = (uint)leapFrogUsers[0].Id,
Guid = leapFrogUsers[0].Guid
};
}
else
throw new HttpResponseException("Invalid login credentials");
}
}
I can browse to myapi/api/users/ and it will call GetAll and I can browse to myapi/api/users/1 and it will call Get, however if I call myapi/api/users/authenticate?username={0}&password={1} then it will call Get (NOT Authenticate) and error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.String Get(Int32)' in 'Navtrak.Services.WCF.NavtrakAPI.Controllers.UsersController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
How can I call custom method names such as Authenticate?
By default the route configuration follows RESTFul conventions meaning that it will accept only the Get, Post, Put and Delete action names (look at the route in global.asax => by default it doesn't allow you to specify any action name => it uses the HTTP verb to dispatch). So when you send a GET request to /api/users/authenticate you are basically calling the Get(int id) action and passing id=authenticate which obviously crashes because your Get action expects an integer.
If you want to have different action names than the standard ones you could modify your route definition in global.asax:
Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = "get", id = RouteParameter.Optional }
);
Now you can navigate to /api/users/getauthenticate to authenticate the user.
This is the best method I have come up with so far to incorporate extra GET methods while supporting the normal REST methods as well. Add the following routes to your WebApiConfig:
routes.MapHttpRoute("DefaultApiWithId", "Api/{controller}/{id}", new { id = RouteParameter.Optional }, new { id = #"\d+" });
routes.MapHttpRoute("DefaultApiWithAction", "Api/{controller}/{action}");
routes.MapHttpRoute("DefaultApiGet", "Api/{controller}", new { action = "Get" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
routes.MapHttpRoute("DefaultApiPost", "Api/{controller}", new {action = "Post"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Post)});
I verified this solution with the test class below. I was able to successfully hit each method in my controller below:
public class TestController : ApiController
{
public string Get()
{
return string.Empty;
}
public string Get(int id)
{
return string.Empty;
}
public string GetAll()
{
return string.Empty;
}
public void Post([FromBody]string value)
{
}
public void Put(int id, [FromBody]string value)
{
}
public void Delete(int id)
{
}
}
I verified that it supports the following requests:
GET /Test
GET /Test/1
GET /Test/GetAll
POST /Test
PUT /Test/1
DELETE /Test/1
Note That if your extra GET actions do not begin with 'Get' you may want to add an HttpGet attribute to the method.
I am days into the MVC4 world.
For what its worth, I have a SitesAPIController, and I needed a custom method, that could be called like:
http://localhost:9000/api/SitesAPI/Disposition/0
With different values for the last parameter to get record with different dispositions.
What Finally worked for me was:
The method in the SitesAPIController:
// GET api/SitesAPI/Disposition/1
[ActionName("Disposition")]
[HttpGet]
public Site Disposition(int disposition)
{
Site site = db.Sites.Where(s => s.Disposition == disposition).First();
return site;
}
And this in the WebApiConfig.cs
// this was already there
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// this i added
config.Routes.MapHttpRoute(
name: "Action",
routeTemplate: "api/{controller}/{action}/{disposition}"
);
For as long as I was naming the {disposition} as {id} i was encountering:
{
"Message": "No HTTP resource was found that matches the request URI 'http://localhost:9000/api/SitesAPI/Disposition/0'.",
"MessageDetail": "No action was found on the controller 'SitesAPI' that matches the request."
}
When I renamed it to {disposition} it started working. So apparently the parameter name is matched with the value in the placeholder.
Feel free to edit this answer to make it more accurate/explanatory.
Web Api by default expects URL in the form of api/{controller}/{id}, to override this default routing. you can set routing with any of below two ways.
First option:
Add below route registration in WebApiConfig.cs
config.Routes.MapHttpRoute(
name: "CustomApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Decorate your action method with HttpGet and parameters as below
[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
string param2, string param3)
{
// your code here
}
for calling above method url will be like below
http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3
Second option
Add route prefix to Controller class and Decorate your action method with HttpGet as below.
In this case no need change any WebApiConfig.cs. It can have default routing.
[RoutePrefix("api/{controller}/{action}")]
public class MyDataController : ApiController
{
[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
string param2, string param3)
{
// your code here
}
}
for calling above method url will be like below
http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3
In case you're using ASP.NET 5 with ASP.NET MVC 6, most of these answers simply won't work because you'll normally let MVC create the appropriate route collection for you (using the default RESTful conventions), meaning that you won't find any Routes.MapRoute() call to edit at will.
The ConfigureServices() method invoked by the Startup.cs file will register MVC with the Dependency Injection framework built into ASP.NET 5: that way, when you call ApplicationBuilder.UseMvc() later in that class, the MVC framework will automatically add these default routes to your app. We can take a look of what happens behind the hood by looking at the UseMvc() method implementation within the framework source code:
public static IApplicationBuilder UseMvc(
[NotNull] this IApplicationBuilder app,
[NotNull] Action<IRouteBuilder> configureRoutes)
{
// Verify if AddMvc was done before calling UseMvc
// We use the MvcMarkerService to make sure if all the services were added.
MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);
var routes = new RouteBuilder
{
DefaultHandler = new MvcRouteHandler(),
ServiceProvider = app.ApplicationServices
};
configureRoutes(routes);
// Adding the attribute route comes after running the user-code because
// we want to respect any changes to the DefaultHandler.
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices));
return app.UseRouter(routes.Build());
}
The good thing about this is that the framework now handles all the hard work, iterating through all the Controller's Actions and setting up their default routes, thus saving you some redundant work.
The bad thing is, there's little or no documentation about how you could add your own routes. Luckily enough, you can easily do that by using either a Convention-Based and/or an Attribute-Based approach (aka Attribute Routing).
Convention-Based
In your Startup.cs class, replace this:
app.UseMvc();
with this:
app.UseMvc(routes =>
{
// Route Sample A
routes.MapRoute(
name: "RouteSampleA",
template: "MyOwnGet",
defaults: new { controller = "Items", action = "Get" }
);
// Route Sample B
routes.MapRoute(
name: "RouteSampleB",
template: "MyOwnPost",
defaults: new { controller = "Items", action = "Post" }
);
});
Attribute-Based
A great thing about MVC6 is that you can also define routes on a per-controller basis by decorating either the Controller class and/or the Action methods with the appropriate RouteAttribute and/or HttpGet / HttpPost template parameters, such as the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace MyNamespace.Controllers
{
[Route("api/[controller]")]
public class ItemsController : Controller
{
// GET: api/items
[HttpGet()]
public IEnumerable<string> Get()
{
return GetLatestItems();
}
// GET: api/items/5
[HttpGet("{num}")]
public IEnumerable<string> Get(int num)
{
return GetLatestItems(5);
}
// GET: api/items/GetLatestItems
[HttpGet("GetLatestItems")]
public IEnumerable<string> GetLatestItems()
{
return GetLatestItems(5);
}
// GET api/items/GetLatestItems/5
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
// POST: /api/items/PostSomething
[HttpPost("PostSomething")]
public IActionResult Post([FromBody]string someData)
{
return Content("OK, got it!");
}
}
}
This controller will handle the following requests:
[GET] api/items
[GET] api/items/5
[GET] api/items/GetLatestItems
[GET] api/items/GetLatestItems/5
[POST] api/items/PostSomething
Also notice that if you use the two approaches togheter, Attribute-based routes (when defined) would override Convention-based ones, and both of them would override the default routes defined by UseMvc().
For more info, you can also read the following post on my blog.
See this article for a longer discussion of named actions. It also shows that you can use the [HttpGet] attribute instead of prefixing the action name with "get".
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
Web APi 2 and later versions support a new type of routing, called attribute routing. As the name implies, attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web API. For example, you can easily create URIs that describe hierarchies of resources.
For example:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Will perfect and you don't need any extra code for example in WebApiConfig.cs.
Just you have to be sure web api routing is enabled or not in WebApiConfig.cs , if not you can activate like below:
// Web API routes
config.MapHttpAttributeRoutes();
You don't have to do something more or change something in WebApiConfig.cs. For more details you can have a look this article.
Just modify your WebAPIConfig.cs as bellow
Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = "get", id = RouteParameter.Optional });
Then implement your API as bellow
// GET: api/Controller_Name/Show/1
[ActionName("Show")]
[HttpGet]
public EventPlanner Id(int id){}

Categories

Resources