Additional GetXYZ methods in EntitySetController - asp.net-web-api

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

Related

Web Api Routing: Multiple controller types were found that match the URL for parameter VS constant paths

My issue is similar to Web Api Routing : Multiple controller types were found that match the URL but I want to keep them in separate controllers.
From the comments, 2 preexisting answers are good workarounds but do not solve the actual issue I'm trying to resolve.
The URLs I'm making up are similar to nested directories in a file system OR are very similar to Firebase URLs.
/BiggestSet/{BiggestSetCode}/Subset1/{Subset1Code}/SubsetOfSubset1/{SubsetOfSubset1}
... etc all the way down to where ever the tree stops. Think of it as a tree of data.
/Collection/{Instance}/Collection/{Instance}
The issue I have is that at the /Collection level I want to also provide specific collection level operations. Like Add and search and other collection specific Operations Collection/ProccessData
Collection Controller:
/Collection/Add
/Collection/ProcessDataOnTheColleciton
Instance Controller:
/Collection/{InstanceCode}
/Collection/{InstanceCode}/ProcessOnTheInstance
The problem I'm having is the Collection/ProcessData clashes with the instance Collection/{InstanceCode}
NOTE: 1 is an parameter and the other is a constant.
If you setup the controllers so that collection and Instance are in the same controller. the /{InstanceCode} doesn't clash with the /ProcessData
BUT
If you setup so the controllers are split into logical functions WebAPI gives the error Multiple controller types were found that match the URL.
Does anyone know how to modify attribute routing to somehow behave as if they are in the same controller OR to prioritize the constant over the parameter across controllers?
To keep two separate controllers and still have such routes you can use regular expression route constraints. This way you can specify for the instanceCode you accept everything except the actions from the other controller.
Here is a sample of how to configure routes like that:
public class CollectionController : ApiController
{
[HttpGet]
[Route("Collection/Add")]
public string Add()
{
return $"CollectionController = Collection/Add";
}
[HttpGet]
[Route("Collection/Process")]
public string Process()
{
return $"CollectionController = Collection/Process";
}
}
public class InstanceController : ApiController
{
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}")]
public string Get(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}";
}
[HttpGet]
[Route("Collection/{instanceCode:regex(^(?!Add$|Process$).*)}/Process")]
public string Process(string instanceCode)
{
return $"InstanceController = Collection/{instanceCode}/Process";
}
}
Here is also a link to the post that explains the regular expression used in the sample.
An even better option would be if you have a specific format for the instanceCode and set the regular expression to accept only this specific format. Then you would not need to modify the regular expression for every new action added. I include also a link to the documentation for all available Route constraints. There you can see all the available options. For example if your instance code is a number you don't even need a regular expression you can just restrict with the int constraint like this [Route("Collection/{instanceCode:int}")].

"the UmbracoHelper was constructed with an UmbracoContext and the current request is not a front-end request."

I'm trying to implement ajax pagination using Umbraco.
On the server side, I have the following:
[System.Web.Http.HttpGet]
public JsonResult pagination(int? page)
{
IEnumerable<IPublishedContent> newsPosts = Umbraco.AssignedContentItem.DescendantOrSelf("news").Children.Where(x => x.IsVisible() && x.DocumentTypeAlias == "newsPost").OrderByDescending(x => x.UpdateDate).Take(5);
//from here on we will be returning the json within which information required for displaying post entries in carousel is included.
string json = "[some random string]"; //just random string for now.
return Json(json, JsonRequestBehavior.AllowGet);
}
As you can see, I'm trying to get necessary data from IPublishedContents, but I'm having trouble instantiating this series of IPublishedContents.
And this is the error I'm getting when I access:
locahost:{port}/umbraco/surface/{controller}/pagination on Chrome.
Cannot return the IPublishedContent because the UmbracoHelper was constructed with an UmbracoContext and the current request is not a front-end request.
Details: System.InvalidOperationException: Cannot return the IPublishedContent because the UmbracoHelper was constructed with an UmbracoContext and the current request is not a front-end request.
As I said, I'm making this request from Chrome, which is I think means this request is from the front end, so I'm not sure why I'm getting this error.
In the course of searching I found these
1) our.umbraco.com forum
2) stackoverflow post
is deserted with no answer, and as for 2, it strikes me that the answer is not quite relevant to my case. I want to instantiate IPublishedContent in the first place.
Mine is Umbraco 7.
and could it be possible to tell me why requests from the front-end are not desirable?
Any hint would be highly appreciated.
Thanks,
Try getting your node this way.
var umbracoHelper = new Umbraco.Web.UmbracoHelper(Umbraco.Web.UmbracoContext.Current);
var yourNode = umbracoHelper.TypedContentAtXPath("umbracoPathtoYourNode");
Perhaps easier to use web api
Create a controller which inherits from UmbracoApiController
public class PagedItemsController : UmbracoApiController
{
[HttpGet]
[ActionName("list")] //Optional see note below
public IHttpActionResult GetItems([FromUri] int pageNo = 1)
{
// Next you need some way of getting the items you need.
// I would not return the whole IPublishedContent items. Rather query those and then use linq Select to transform into a more relevant smaller class (not doing this here)
// I've just included this for brevity
var items = _itemService.GetPagedItems(pageNo);
// Now return the results
return Ok(items);
}
}
Calls to endpoints in Umbraco follow the format
/umbraco/api/{controller}/{endpoint}
With the [ActionName("list")] above the call to the GetItems method will be
http://example.com/umbraco/api/PagedItems/list?pageNo=3
Without the ActionName attribute the call would be
http://exampe.com/umbraco/api/PagedItems/GetItems?pageNo=3
With a standard jquery ajax call this will return json without needing to serialise.

How to override a web api route?

I am trying to standardize an extension model for our REST API development team. We need to provide default implementation of routes, while allowing for custom implementations of routes that replace the default as well.
As a simple example if we have a GET route api/users like this:
public class DefaultUsersController : ApiController
{
[HttpGet]
[Route("api/users", Order = 0)]
public IEnumerable<string> DefaultGetUsers()
{
return new List<string>
{
"DefaultUser1",
"DefaultUser2"
};
}
}
We expect the default work like this:
Now a developer wants to change the behavior of that route, he should be able to simply define the same route with some mechanism to imply their implementation should be the one used, instead of the default. My initial thinking was to use the Order property on the Route attribute since that's what it appears to be there for, as a way to provide a priority (in ascending order) when an ambiguous route is discovered. However it's not working that way, consider this custom implementation that we want to override the default api/users route:
public class CustomUsersController : ApiController
{
[HttpGet]
[Route("api/users", Order = -1)]
public IEnumerable<string> CustomGetUsers()
{
return new List<string>
{
"CustomUser1",
"CustomUser2"
};
}
}
Notice the Order property is set to -1 to give it a lower priority value than the default, which is set to 0. I would have thought this would be used by the DefaultHttpControllerSelector, but it isn't. From the DefaultHttpControllerSelector:
And we end up with this exception being returned in the response:
Is it possible Microsoft just missed the logic/requirement to use Order as a route disambiguator and this is a bug? Or is there another simple way to override a route, hopefully with an attribute?
I have pretty much the same problem. I am creating a starter site, but I want users to be able to redefine to behaviour of a Controller, especially if there is a bug.
I use Autofac to resolve the Controller, but even when I register the new controller as the old one, the original one gets selected.
What I'll do is probably go with URL Rewriting. Especially since this issue is temporary in my case. However, I would be interested if someone has a better option.

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=

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

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.

Resources