Cannot implement multiple GET methods in WebApi OData - asp.net-web-api

I'm using OData V3 with MVC4 Web API project .NET4.
The WebAPI register method is:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling =
Newtonsoft.Json.PreserveReferencesHandling.None;
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<ClientModel>("ODClient");
builder.ComplexType<ClientStatus>();
builder.ComplexType<ClientType>();
var edmmodel = builder.GetEdmModel();
config.Routes.MapODataRoute(
routeName: "odata",
routePrefix: "odata",
model: edmmodel
);
}
The OData controller is:
[HttpGet]
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All, PageSize = 25)]
public IQueryable<ClientModel> Get()
{
var model = ...
return model;
}
[HttpGet]
public ClientModel Get([FromODataUri] int id)
{
return new ClientModel();
}
[HttpDelete]
public void Delete([FromODataUri] int id)
{
}
This query runs well:
http://localhost:59661/odata/ODClient?$filter=id eq 3
But this query doesn't work:
http://localhost:59661/odata/ODClient(3)
It executes first GET query with all items.
The Delete doesn't work either (the request type is DELETE):
http://localhost:59661/odata/ODClient(3)
The error received is:
"No HTTP resource was found that matches the request URI 'http://localhost:59661/odata/ODClient(12)'."

As per question comments, the issue was the way that the default routing conventions assigns a name to parameters. Keys are actually given the default name of "key" and so switching to that worked.
The name can be customized by either creating a custom routing convention that populates the route data table with a "id" value, or by using attribute based routing in which case the parameter name can match the name specified in the path template.

Related

MVC Web API Routing to Wrong Action

I have a web API controller, when I call the default Get action this works, when I call another specific action (GetReservationsForCustomer) this also works, but one action gives an error (GetReservationsByDate), it seems to route to the default Get action. Here is the code:
// GET: api/Reservations
public IQueryable<Reservation> GetReservations()
{
return db.Reservations;
}
[ResponseType(typeof(ReservationDTO))]
public IHttpActionResult GetReservationsForCustomer(int CustomerId)
{
IEnumerable<Reservation> reservations = db.Reservations.Where(r => r.CustomerId == CustomerId).ToList();
List<ReservationDTO> reservationList = new List<ReservationDTO>();
foreach(Reservation reservation in reservations)
{
reservationList.Add(new ReservationDTO
{
id = reservation.id,
ReservationStart = reservation.ReservationStart,
Covers = reservation.Covers
});
}
return Ok(reservationList);
}
[ResponseType(typeof(ListReservationDTO))]
public IHttpActionResult GetReservationsByDate(DateTime StartDate, DateTime EndDate)
{
IEnumerable<Reservation> reservations = new List<Reservation>();
if (EndDate != null)
{
reservations = db.Reservations.Where(r => r.ReservationStart.Date >= StartDate.Date && r.ReservationStart.Date >= EndDate.Date).ToList();
}
else
{
reservations = db.Reservations.Where(r => r.ReservationStart.Date == StartDate.Date).ToList();
}
List<ReservationDTO> reservationList = new List<ReservationDTO>();
foreach (Reservation res in reservations)
{
reservationList.Add(new ReservationDTO
{
id = res.id,
ReservationStart = res.ReservationStart,
Covers = res.Covers,
CustomerEmail = res.Customer.EmailAddress,
CustomerName = res.Customer.Name,
CustomerPhone = res.Customer.PhoneNumber
});
}
return Ok(reservationList);
}
Here is my API call:
http://localhost:55601/api/Reservations/GetReservationsByDate/?StartDate=2018-03-04:T12:30:00
And here is the response:
{
"Message": "The request is invalid.",
"MessageDetail": "The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Http.IHttpActionResult GetReservation(Int32)' in 'GreenLionBookings.API.ReservationsController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."
}
Please note the specifics of the action are not relevant at this stage, I've butchered it a fair bit trying to get this to work! I've tried specifying a start date and end date and neither seems to work. It always seems to get routed to the default Get action.
Here is my RouteConfig:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
And here is my WebApiConfig:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
So both basically just default.
Why is this not routing to the correct action, just for this one and not the others? I have another controller (Customers) which seems to work correctly for all actions too. I've read this and this, and also this and this which I actually thought was pretty relevant and quite helpful, but didn't solve my problem.
What am I doing wrong here?
First of all, you have a typeo in the date.
This 2018-03-04:T12:30:00 should be 2018-03-04T12:30:00.
Then, to solve the routing problem, you could leave out the action name of the url and let the framework match the request against the parameters name.
Try it like this
api/Reservations?StartDate=2018-03-04T12:30:00&EndDate=2018-03-05T12:30:00
Then, if you want to be able to send nullable values to EndDate which is a value type of DateTime; make the DateTime nullable
[ResponseType(typeof(ListReservationDTO))]
public IHttpActionResult GetReservationsByDate(DateTime StartDate, DateTime? EndDate)
Notice the DateTime? which is a shorthand for Nullable<DateTime>

MVC Web Api 404 Bad Request

I have a problem about MVC WebAPi. Here some information from my project.
WebApiConfig;
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}/{no}",
defaults: new { id = RouteParameter.Optional, no = RouteParameter.Optional }
TapuZeminApiController;
// GET api/<controller>
[Route("api/TapuZeminApi/GetZemins")]
[HttpPost]
public string GetZeminsFromZeminArg(object arg)
{
ZeminArg zemArg = SConvert.DeserializeJSON<ZeminArg>(arg.ToString());
List<TapuZeminModel> zeminList = TapuModule.GetZeminListFromArgs(zemArg);
string jsonResult = SConvert.SerializeJSON(zeminList);
return jsonResult;
}
// GET api/<controller>/5
public string GetZeminsFromTcNo(int id)
{
List<TapuZeminModel> zeminList = TapuModule.GetZeminListFromTcNo(id.ToString());
string jsonResult = SConvert.SerializeJSON(zeminList);
return jsonResult;
}
And i have another a lot of Api like;
TapuParselApiController;
public List<string> GetAdaNo(int id)
{
List<string> adaList = TapuModule.GetAdaListFromMahalleTapuKod(id);
adaList = adaList.OrderBy(x => x, new AlphanumComparator()).ToList();
return adaList;
}
[Route("Api/TapuParselApi/GetParselNo/MahalleId/{id}/AdaNo/{adaNo}")]
public object GetParselNo(int id, string adaNo)
{
List<TapuParselModel> parselList = TapuModule.GetParselListFromMahalleAndAdaTapuKod(id, adaNo);
List<string> parselNoList = parselList.Select(x => x.ParselNo).ToList<string>();
parselNoList = parselNoList.OrderBy(x => x, new AlphanumComparator()).ToList();
var jsonResult = SConvert.SerializeJSON(parselNoList);
return jsonResult;
}
I can use all of api but one of them not working. When i tried to reach
http://localhost:55591/Api/TapuZeminApi/GetZeminsFromTcNo/41206410132
it returns
This XML file does not appear to have any style information associated with it. The document tree is shown below.
The request is invalid.
<MessageDetail>
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.String GetZeminsFromTcNo(Int32)' in 'Sehir.Catalog.Areas.Tapu.Controllers.TapuZeminApiController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
</MessageDetail>
</Error>
Why is getting this error?? What should i do?
http://localhost:55591/Api/TapuZeminApi/GetZeminsFromTcNo/41206410132
The value 41206410132 in this http request is too large for an int.
You need to change the parameter type in your controller action to a long.
// GET api/<controller>/5
public string GetZeminsFromTcNo(long id)
{
List<TapuZeminModel> zeminList = TapuModule.GetZeminListFromTcNo(id.ToString());
string jsonResult = SConvert.SerializeJSON(zeminList);
return jsonResult;
}
You can read more about the limits of numeric types here.

Can I have non Rest styled Methods in web api?

My question specifically is if i can support something like this
public class SomeTestController : ApiController
{
[System.Web.Http.AcceptVerbs("GET")]
public SomeInfo GetSomeBlah(int a, int b)
{
return new SomeInfo
{
Name = string.Format("GetSomeBlah - {0}", a),
CompanyID = b.ToString()
};
}
[System.Web.Http.AcceptVerbs("GET")]
public SomeInfo GetSomeBlah1(int a, int b)
{
return new SomeInfo
{
Name = string.Format("GetSomeBlah1 - {0}", a),
CompanyID = b.ToString()
};
}
}
I tried to add the following route
config.Routes.MapHttpRoute(
name: "DefaultApi4",
routeTemplate: "api/{controller}/{action}",
defaults: new {action = RouteParameter.Optional}
);
Edit: 4/7/15
I forgot to mention it should also work with rest styled methods.
Not sure why you are trying to do this, it sounds like you have misunderstood how routing works - Have you seen these tutorials on Routing and how ASP.net maps routes?
From point 3b below this means that if you name your input varibles more meaningfully, the framework will be able to map on the name and type coming in from the route as to which method to pick i.e. getSomeBlah or GetSomeBlack1
Summary for others,
1). Create a list of all actions on the controller that match the HTTP request method.
2). If the route dictionary has an "action" entry, remove actions whose name does not match this value.
3). Try to match action parameters to the URI, as follows:
a). For each action, get a list of the parameters that are a simple type, where the binding gets the parameter from the URI. Exclude optional parameters.
b). From this list, try to find a match for each parameter name, either in the route dictionary or in the URI query string. Matches are case insensitive and do not depend on the parameter order.
c). Select an action where every parameter in the list has a match in the URI.
d). If more that one action meets these criteria, pick the one with the most parameter matches.
4). Ignore actions with the [NonAction] attribute.
That will work if you remove the default route. I also indicated where the parameters are from.
Specify the input is from the URL:
public SomeInfo GetSomeBlah([FromUri] int a, [FromUri] int b)
Comment or remove this:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Test URL:
http://localhost:64719/api/Values/GetSomeBlah?a=1&b=2
Response:
<ValuesController.SomeInfo xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/SampleMVC.Controllers"> <CompanyID>2</CompanyID><Name>GetSomeBlah - 1</Name> </ValuesController.SomeInfo>
configure it like this:
config.Routes.MapHttpRoute("DefaultApi2",
"apiws/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute("DefaultApi",
"apiws/{controller}/{id}",
new { id = RouteParameter.Optional });
this works for both rest style and non-rest styles apis.
Note that the sequence of the two statements matters.
I managed to get it to work with the following code
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
and Attribute routing.
[RoutePrefix("api/someother")]
public class SomeOtherController : ApiController
{
// GET: api/SomeOther
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET: api/SomeOther/5
public string Get(int id)
{
return "value";
}
// POST: api/SomeOther
public void Post([FromBody]string value)
{
}
// PUT: api/SomeOther/5
public void Put(int id, [FromBody]string value)
{
}
[System.Web.Http.HttpGet()]
[System.Web.Http.Route("Findsomething2")]
public CompanyInfo Findsomething2(int id, int b)
{
return new CompanyInfo
{
CompanyID = b.ToString(),
Name = id.ToString() + " 2"
};
}
// DELETE: api/SomeOther/5
public void Delete(int id)
{
}
}

Web API Help is not generated as expected

[RoutePrefix("Office")]
public class OfficeController : ApiController
{
[Route("~/Admin/{adid}")]
public string GetAdminById(int adid)
{
return string.Format("Enter value is {0}", adid);
}
[Route("~/User/{uid}")]
public string GetUserByName(int uid)
{
return string.Format("Enter value is {0}", uid);
}
}
but help is generated below
GET api/Office?adminid={adid}
GET api/Office?userid={uid}
but I am excepting
GET Office/Admin/adid
GET Office/User/uid
For this I added one more route, for admin at webapi.config
config.Routes.MapHttpRoute(
name: "OfficeadminApi",
routeTemplate: "api/{controller}/Admin/{adid}",
defaults: new { **adid** = RouteParameter.Optional }
);
then 2 signatures are generated by web api help are
GET api/Office?adid={adid}
GET api/Office/Admin/{adid} // I expected is generated but
what is the use of Attribute routing, if added no'f Routes at webapi.config?
as per Attribute routing it should generates, routes specified at attribute level.
Please clarify on this any one please ?

RouteHandler vs ControllerFactory

new to asp.net mvc (using v3 + razor) and am wondering how to best solve a problem with creating dynamic routes based on a database. Essentially, the main site navigation will be entered into a database and I want to load them up as routes. i.e. - Load Category list from database, then append the routes to the routing engine if possible...
mysite.com/cars
mysite.com/televisions
mysite.com/computers
etc....
Each category after the slash comes from the db, but, there are regular entries like /about and /contactus that will not be in the database and have been statically entered in the global.asax... my question is:
For the dynamic database URLs should I use a custom RouteHandler or pehaps create a ControllerFactory that will match and handle the requests for the entries loaded from the database. Is it possible to have the DefaultControllerFactory handle the routing if my RouteHandler or CustomControllerFactory don't find the route in the list from the database? Thanks for any help, very first project with this so I'm not sure what the best route is ;) no pun intended...
Update:
Tried using a route constraint that pulls from the database but it conflicts with the default route now... here is my custom constraint and routes:
public class CategoryListConstraint : IRouteConstraint
{
public CategoryListConstraint()
{
var repo = new Repository<Topic>();
var cats = repo.All();
var values = new List<string>();
foreach (var c in cats)
{
values.Add(c.URI.Replace("/", "").Replace("?", ""));
}
this._values = values.ToArray<string>();
}
private string[] _values;
public bool Match(HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
// Get the value called "parameterName" from the
// RouteValueDictionary called "value"
string value = values[parameterName].ToString();
// Return true is the list of allowed values contains
// this value.
return _values.Contains(value);
}
}
and here are the routes:
Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Categories",
"{category}/{*values}",
new { controller = "Category", action = "List" },
new CategoryListConstraint()
);
Routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
The home page www.mysite.com loads using the Default route. All the URLs that match the constraint list are loaded by the category route... but if I have the www.mysite.com/admin or www.mysite.com/aboutus these are getting picked up by the Categories route even though the values are not in the constraint list. Confused...
What about something like this?
Categories controller:
public ActionResult List(string category)
{
var products = _repo.Get(category); // however you are getting your data
return View(products);
}
Routes
routers.MapRoute(
"About",
"About",
new { controller = "Home", action = "About" });
//... other static routes
routes.MapRoute(
"CategoriesList",
"{id}",
new { controller = "Categories", action = "List" },
new { id = #"\w+" });
The incoming URL is tested against each Route rule to see if it matches - and if a Route rule matches then that rule (and its associated RouteHandler) is the one that is used to process the request (and all subsequent rules are ignored). This means that you want to typically structure your routing Rules in a "most specific to least specific" order
source
Found the exact solution I was looking for. Code is below. I managed to avoid using Controller Factories or implementing a custom IRouteHandler by using extending the RouteBase class which worked perfectly and allows me to pass control down to the default mvc route is something specific isn't hit. BTW - constraints ended up not working properly as the broke the controllers associated with the default route (although the default route was getting hit)
public class CustomRoutingEngine : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeHandler = new MvcRouteHandler();
var currentRoute = new Route("{controller}/{*URI}", routeHandler);
var routeData = new RouteData(currentRoute, routeHandler);
// implement caching here
var list = GetConstraintList();
// set your values dynamically here
routeData.Values["controller"] = "Category";
// or
routeData.Values.Add("action", "List");
// return the route, or null to have it passed to the next routing engine in the list
var url = Util.StripSlashOnFrontAndBack(httpContext.Request.Path.ToLower()).Split('/')[0];
if (list.Contains(url))
return routeData;
return null; // have another route handle the routing
}
protected List<string> GetConstraintList()
{
using (var repo = new RavenRepository<Topic>())
{
var tops = repo.Query().Where(x => x.Hidden == false).ToList()
.Select(x=>x.Name.ToLower());
List<string> list = new List<string>();
list.AddRange(tops);
repo.Dispose();
return list ?? new List<string>();
}
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//implement this to return url's for routes, or null to just pass it on
return null;
}
}
Then my register routes method looks like so:
Routes.Clear();
// Set Defaults
Routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
AreaRegistration.RegisterAllAreas();
routes.Add(new App.Helpers.CustomRoutingEngine());
Routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

Resources