ASP.NET WebAPI OData - Inheriting from EntitySetController<> but using Get(ODataQueryOptions options) rather than [Queryable]Get() - asp.net-web-api

I'm using the ASP.Net WebAPI nightly build (2013-01-16) to get the latest OData support possible.
As the Meta-Me blog on MSDN OData 0.2.0-alpha release post says, there's now an EntitySetController<T> from which OData controllers can be derived to take away a lot of the pain and plumbing code.
The EntitySetController<T> class implements Get() as
[Queryable]
public virtual IQueryable<TEntity> Get()
{
throw EntitySetControllerHelpers.GetNotImplementedResponse(Request);
}
I'd like to make use of the more specific Get(ODataQueryOptions options) method offered by the ASP.Net Web API OData support.
I've coded it as
public IEnumerable<Patient> Get(ODataQueryOptions options)
{
IQueryable patients = entities.Patients;
if (options.Filter != null)
{
patients = options.Filter.ApplyTo(patients, new ODataQuerySettings());
}
return (patients as IQueryable<Patient>).AsEnumerable();
}
(I've also had this return IQueryable<> and saw someone else talk about an ODataResult - that's a type I can't discover at the moment).
However, if I try to use the ODataQueryOptions-based Get method in my own controller I get an error message about multiple actions matching the request. Specifically that error is
Multiple actions were found that match the request:
System.Collections.Generic.IEnumerable`1[Dox.Server.Model.Patient] Get(System.Web.Http.OData.Query.ODataQueryOptions) on type Dox.Server.Web.Controllers.PatientController
System.Linq.IQueryable`1[Dox.Server.Model.Patient] Get() on type System.Web.Http.OData.EntitySetController`2[[Dox.Server.Model.Patient, Dox.Server.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
I assume this is due to the route resolver (sorry if that's poor ASP.NET routing terminology) seeing Get() or Get(...) on the controller's base class as well as the controller class itself.
Questions:
a) Is there some way of adjusting routes to fix this?
b) If not, should I make my own version of EntitySetController<T> and swap out it's Get() method?
The configuration being called by Application_Start() is limited to
public static void EnableOData( HttpConfiguration config )
{
var model = BuildModelImplicitly(config);
//As per LinqPad forum: http://forum.linqpad.net/discussion/178/odata-v3-not-working
IEdmEntityContainer container = model.EntityContainers().First();
model.SetIsDefaultEntityContainer(container, true);
//config.EnableOData(model, "api");
config.Routes.MapODataRoute("OData", "api", model);
//config.EnableSystemDiagnosticsTracing();
}
There's no other configuration being called to do with routes or handlers, etc. Note that the EnableOData() method on HttpConfiguration no longer exists in the latest nightly builds as per this CodePlex discussion.
Thanks very much!

It's very cool to see that you're using our nightly builds :)
The reason you're getting a multiple matching actions error is because EntitySetController already defines a Get method. The good news is that EntitySetController also defines a QueryOptions property that you can use to retrieve the query options. So you should be able to override the EntitySetController's Get method and use the query options property instead of the parameter. It should behave exactly the same way as if you had bound the query options to an action parameter.

Related

How do I expose a navigation property over OData 4 and WebApi 2.2?

I have a navigation property on a model, Site.Locality and although its foreign key is serialized and available to consumers (Site.LocalityName) I'd like the locality itself to be available from:
~/Site('A')/Locality
How is this done in OData v4 over WebApi 2.2?
On your controller for the Site entity, add the following action:
// Implies that the controller has [ODataRoutePrefix("Sites")]
[ODataRoute("({name})/Locality")]
public async Task<Locality> GetLocality([FromODataUri] string name)
{
// Add try-catch or null 404 handling.
var site = await this.Repository.GetAsync(new[] { name });
return site.Locality;
}
Obviously, place your own DAL code in there, this is just an example.
It's very clear to see that this is achieved through nothing more complex than a simple route and action on your controller.
That said, there is some mapping happening under the hood. For example, you couldn't just expose any arbitrary navigation property:
[ODataRoute("({name})/Wangachop")]
public string GetWangachop([FromODataUri] string name)
{
return "Wangaaa!";
}
Would yield:
The path template 'Sites({name})/Wangachop' on the action 'GetWangachop' in controller 'Sites' is not a valid OData path template. Found an unresolved path segment 'Wangachop' in the OData path template 'Sites({name})/Wangachop'.

Additional GetXYZ methods in EntitySetController

I want to be able to have additional GetXYZ methods in my EntitySetController derived controller class. For example:
[HttpGet]
[Queryable]
public string GetAirportsWithinRadius(int airportId, int radius)
{
var resultAirports = GetAirportsWithinRadius2(airportId, radius);
return resultAirports;
}
This is what I have for config:
ActionConfiguration getAirportsWithinRadius = modelBuilder.Entity<Airport>().Collection.Action("GetAirportsWithinRadius");
getAirportsWithinRadius.Parameter<int>("airportId");
getAirportsWithinRadius.Parameter<int>("radius");
getAirportsWithinRadius.ReturnsCollectionFromEntitySet<Airport>("Airports");
I want this action to be composable just like the default Get Queryable action, but this would be an alternative that supports all odata parameters but additionally an airportId and radius. This would first filter airports by a radius search (this I know how to do - it's irrelevant to the question) and then return the Queryable so that it can be further filtered by odata params.
Everything I read says this would be an odata action and therefore must be a POST, but Get is also an action and that is a GET, so why not allow extended getters with additional parameters? Am I missing something? How do I accomplish what I want to get done?
I would call this from an ajax client as such:
GET /odata/Airports?$inlinecount=allpages&$top=25&airportId=2112&radius=50
as opposed to a regular odata GET:
GET /odata/Airports?$inlinecount=allpages&$top=25
Thanks
EDIT:
I understand now that this is an odata "function" and it is under consideration as a future feature. Let's forget for second the odata meaning of this. It is essentially a WebApi HttpGet that returns a Queryable, right? So, as long as I don't care about the metadata advertising of this "function", how can I make sure that it is a reachable HttpGet form a route perspective inside of an ODataController? The ODataController needs the MapODataRoute and can I additionally add non odata routes using additional MapHttpRoutes?
I ask this because it seems to me that I should be able to, but all my tries have failed (trying to hit the HttpGet through fiddler). I can find no examples on extending an ODataController with additional non-odata GETs. Can someone help me understand if and how this can be done with the example?:
[Queryable]
public IQueryable<Airport> Get()
{
return db.Airports;
}
[HttpGet]
[Queryable]
public string GetAirportsWithinRadius(int airportId, int radius)
{
var resultAirports = GetAirportsWithinRadius2(airportId, radius);
return resultAirports;
}
You are looking for OData Functions, which is not yet supported out of the box. We have an issue over here. You can up-vote it.
http://aspnetwebstack.codeplex.com/workitem/881

Return JsonNet type JSON from WebAPI in Orchard

In standard MVC I use JsonNet to return JSON that is in camelCase and sucessfully serializes entities that have related entities (which otherwise reports a "cycles" error" using the default serializer).
I'd like to do the same for a WebAPI controller in an Orchard module. By default it returns PascalCase JSON and reports a "cyles" exception when given a list of entities.
Can anyone explain how best to configure the JSON output from within the Orchard module, to mimic what JsonNet would produce?
I've found a workaround, which is to set the JSON formatters settings to camelCase in an ActionFilter:
public class CamelCaseJsonAttribute : ActionFilterAttribute {
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var jsonFormatter = actionContext.ControllerContext.Configuration.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
But this seems inefficient, as it gets set on each request, rather than globally, once.
I'm guessing there is an extensibility point somewhere in a module to set the HttpConfiguration - can anyone tell me one way or the other?
Many thanks.

Controller not filtering data in Breeze query in DotNetNuke Module

I am trying to include the basic Breeze sample in a DotNetNuke module (it works fine in a standalone WebAPI project). To simplify things I remove the client and will just refer to the URL JSON calls I make in the Chrome browser.
I can see my metadata and a full list of items, eg:
http://www.dnndev.me/DesktopModules/framework/api/breeze/dare/metadata
http://www.dnndev.me/DesktopModules/framework/api/breeze/dare/todos
however, when I try to filter the list from the URL, it always returns the full list, e.g.
http://www.dnndev.me/DesktopModules/framework/api/breeze/dare/todos?=DareId%20eq%204
I think it is something to do with the way I have declared the MapHTTRoute. The problem is that DotNetNuke modules do not have a Global.ascx. I have copied the BreezeWebApiconfig.cs file into my App_Start folder and this does fire when I debug, however DotNetNuke uses mechanism for registering routes:
using DotNetNuke.Web.Api;
namespace SmartThinker.Modules.Framework
{
public class RouteMapper : IServiceRouteMapper
{
public void RegisterRoutes(IMapRoute mapRouteManager)
{
mapRouteManager.MapHttpRoute("framework", "BreezeApi", "breeze/{controller}/{action}", new[] { "SmartThinker.Modules.Framework.Controllers" });
}
}
}
I have read up on http://www.breezejs.com/documentation/web-api-controller#note01 and http://www.breezejs.com/documentation/web-api-routing but it seems that it's something to do with the way DNN registers the routes. Is there anyway to do this without using BreezeWebApiConfig.cs?
My controller code has the BreezeController attribute. (When I do connect the sample client to it I do get a list of items - it just does not filter, so I think it is something to with the OData Action filters. How can I debug where the problem is?
Update 1)
Here is the metadata:
http://www.ftter.com/desktopmodules/framework/api/dare/metadata
The GetUsers method:
http://www.ftter.com/desktopmodules/framework/api/dare/getusers
and the GetUsers method trying to filter by UserID (which doesn't work, which is the issue)
http://www.ftter.com/desktopmodules/framework/api/dare/getusers?=UserID%20eq%204
http://www.ftter.com/desktopmodules/framework/api/dare/GetUsersWithoutCors?=UserID%20eq%204 (this returns IQueryable)
Here is the controller:
[BreezeController]
public class DareController : DnnApiController
{
private readonly EFContextProvider<FrameworkContext> contextProvider = new EFContextProvider<FrameworkContext>();
[AllowAnonymous]
[HttpGet]
public HttpResponseMessage Metadata()
{
var response = Request.CreateResponse(HttpStatusCode.OK, contextProvider.Metadata());
return GetResponseWithCorsHeader(response);
}
[AllowAnonymous]
[HttpGet]
public HttpResponseMessage GetUsers()
{
var userInfoController = new UserInfoController();
var response = Request.CreateResponse(HttpStatusCode.OK, userInfoController.GetUsers());
return GetResponseWithCorsHeader(response);
}
[AllowAnonymous]
[HttpGet]
public IQueryable<User> GetUsersWithoutCors()
{
return contextProvider.Context.Users;
}
}
The routing is not really a Breeze issue. How your server routes requests to your controller is up to you. What we do out-of-the-box is just one way among innumerable many.
You have the [BreezeController] attribute on your controller yes? Can you put a sample endpoint up where we could hit it. Might get some clues from that. Also post the controller. A tiny example should do ... something returning metadata and one method returning IQueryable.
Update 25 Jun 2013
I think you've discovered a bug in the way our [BreezeController] discovers methods returning IQueryable<T>.
The [BreezeController] attribute scans your Web API controller methods and (in effect) applies the [BreezeQueryable] attribute to methods returning IQueryable<T>.
[BreezeQueryable] is an extension of the Web API's [Queryable] that adds support for $select, $expand, and nested $orderby ... all missing from the current [Queryable].
I see now that your GetUsers() method returns HttpResponseMessage rather than IQueryable<User>. Let's assume that the userInfoController.GetUsers() method inside your method returns IQueryable<User>. Otherwise, the OData query parameters will not apply and we'll have to take this in a different direction. Moving along ...
I checked with v.1.3.6 of the Breeze.WebApi.dll and it does not detect that the HttpResponseMessage is wrapping IQueryable<T>. Therefore, it does not apply the client's OData query criteria (or any other OData modifiers for that matter). This shortcoming (in my opinion) is a bug. The following should be equivalent implementations:
[HttpGet]
public IQueryable<TodoItem> Todos() {
return _repository.Todos;
}
[HttpGet]
public HttpResponseMessage TodosWrapped()
{
return Request.CreateResponse(HttpStatusCode.OK, _repository.Todos);
}
The second, "wrapped" method does not respect the OData query parameters.
Fortunately, there is a workaround until we get this fixed. Just add the [BreezeQueryable] attribute explicitly ... as in:
[HttpGet]
[BreezeQueryable]
public HttpResponseMessage TodosWrapped()
{
return Request.CreateResponse(HttpStatusCode.OK, _repository.Todos);
}
I confirmed that this approach does work.
Thanks for finding this.
Use OData query syntax
A colleague also noticed that your query URL does not use the OData query syntax. You wrote:
... /todos?=DareId%20eq%204
when it should be
... /todos/?$filter=DareId%20eq%204
Make sure you use ?$filter=

The "DELETE" type of Http request does not work in WebAPI?

I have GET, PUT, POST working in my WebAPI project.
The last one of Http requests I am doing is DELeTE, BUT it does not work.
I have read through many posts in here as well as other websites, none of them. e.g.
WebAPI Controller is not being reached on DELETE command
WebAPI Delete not working - 405 Method Not Allowed
ASP.Net WebAPI Delete verb not working
ASP.NET Web API - PUT & DELETE Verbs Not Allowed - IIS 8
http://social.msdn.microsoft.com/Forums/en-US/windowsazuredevelopment/thread/8906fd7e-a60b-484e-be63-9574b9fca44a/
etc...
Are there any workarounds?
Please help, thanks.
Update:
My back-end code:
[HttpDelete]
public HttpResponseMessage Delete(int divisionID)
{
if (divisionID != default(int))
{
var found = dc.MedicareLocalAccounts.SingleOrDefault(m => m.DivisionID == divisionID);
if (found == null)
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
dc.MedicareLocalAccounts.Remove(found);
dc.SaveChanges();
return new HttpResponseMessage(HttpStatusCode.OK);
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
Now, if I change the parameter type from int to any classes, let's say Division
Delete(Division d)
{
int divisionID = d.DivisionID;
//....the rest is same
}
In this way, it works.
But I just do not want to input the entire object as a parameter to make the DELETE method work as it is not necessary.
So do you have any other better solutions?
Web API handles simple parameter types (int) differently than complex types (classes). By default, a simple parameter is taken from the request URI, and a complex type is taken from the request body.
In your first example, the parameter name is 'divisionID' -- does this match your route variable? The default Web API route is "api/{controller}/{id}", so the parameter should be named 'id'.
A workaround would be using the AttributeRouting library. This is an extension to WebAPI and can be downloaded from nuget. With the AttributeRouting library you could e.g. implement a function with HttpGet that wil perform the delete
[GET("delete/{id}"]
function DeleteThis(int id)
{
...
}

Resources