Is there a way in webapi(besides odata) to specify multiple queries?
Here is the scenario:
A custom sproc. that accepts 2 zip codes, performs distance calculation and return the distance
The WebAPI endpoint accepts the zips and calls into the sproc. and returns a Distance DTO
class ZIPDistance
{
public string Zip1,
public string Zip2,
public sttring Distance
}
The URL for this: "/api/ZipDistances?$select=Distance&$filter=Zip1 eq 13240 and Zip2 eq 90210
In the ZipDistancesController we extract the parameters from the above query using ODataQueryOptions, call into the sproc.
and return the DTO above.
For potential performance reasons it was requested that the above endpoint somehow accept a collection of zip code pairs i.e. a collection
of the "ZIPDistance" DTOs representing the "pairs" of zips for which we need to call into the sproc above, loop over the DTOs, invoke the sproc. per DTO and return a collection of DTOs for the result
so that a client would make a single HTTP call instead of multiple calls and obtain all results in 1 call.
The only way that I am aware of doing is via a POST and pass in the collection of ZIPDistances representing the zip code pairs in the
payload, but this is fundamentally against REST since now we change the semantic meaning of POST to mean data retrieval (i.e. GET)
The question is whether the above scenario supported by WEBAPI and what would be the best way for implementing the above without overloading
verb meaning and causing confusion.
UPDATE:
I've prototyped out one possible solution here which involves embedding the pairs in the URL:
http://<host>/api/ZipDistances?$select=Distance&$filter=Pairs eq 'Zip1 eq 13240 and Zip2 eq 90210,Zip1 eq 13241 and Zip2 eq 90211'
and the corresponding DTO:
public class ZipDistanceDTO
{
public string ZipPairs { get; set; }
public string Distance { get; set; }
}
This would return the following result:
[{"ZipPairs":"'Zip1 eq 13240 and Zip2 eq 90210","Distance":"558"},
{"ZipPairs":"Zip1 eq 13241 and Zip2 eq 90211'","Distance":"558"}]
Comments/Thoughts on this would be appreciated.
UPDATE (2):
I've posted another prototype here that uses a Query resource
Issuing proper GET request with multiple ZIP codes is just fine to be used.
However,
Another possible RESTfull way is to create a "Query Resource" object using POST, returning "Query Resource ID Number", which will later be used in a separated GET request.
By that you are creating saved queries which can also be efficient for re-querying as well.
Related
I am playing around with REST services in a Spring Boot environment. I have a question about the URI and naming conventions.
I have (currently) the following three mappings in my controller implementation...
#RequestMapping(method=RequestMethod.GET, value= "/v1/accounts")
public List<Account> getAllAccounts() {
return accountService.getAllAccounts();
}
#RequestMapping(method=RequestMethod.GET, value="/v1/accounts/{accountId}")
public Account getAccount(#PathVariable int accountId) {
return accountService.getAccount(accountId);
}
#RequestMapping(method=RequestMethod.GET, value="/v1/accounts/")
public Account getAccount(#RequestParam("shortName") String shortName) {
return accountService.getAccount(shortName);
}
These currently "work", but I have a question/concern about the getAccount(String) method. If I simply use the path "v1/accounts", the compiler seems to be unable to differentiate this from the URI for getAllAccounts(). So, I added the trailing '/', and the request now looks like...
/v1/accounts/?shortName=foo
However, it seems like the two requests should be...
/v1/accounts
and
/v1/accounts?shortName=foo
But, as already identified, changing the third request mapping to remove the trailing '/' results in compile-time errors.
Any input on either (a) how to eliminate the trailing '/' without running into compile-time errors, or (b) the advisability of incorporating the trailing '/' "just" to have both REST services exposed (I'm concerned about "what happens when the 3rd service is needed")?
After talking with one of our front-end devs, who said it's preferable to only deal with one return type (i.e., List vice List and Account), things simplified to...
#RequestMapping(method=RequestMethod.GET, value="/v1/accounts")
public List<Account> getAccount(#RequestParam("shortName") Optional<String> shortName) {
return (shortName.isPresent()) ? accountService.getAccount(shortName.get())
: accountService.getAccounts();
}
I've seen some concerns about the use of "Optional" as a parameter type, but it does seem to (a) clean up the REST endpoint's URI, and (b) is not horrific in the controller implementation.
Unless someone points out the "big, unanticipated nightmare" associated with this approach, I think I'll run with this
In OData V3, I can select just fields from parent/ancestor entities like this:
http://services.odata.org/V3/Northwind/Northwind.svc/Order_Details(OrderID=10248,ProductID=11)?&$select=Product/Category/CategoryName&$expand=Product/Category
That query returns only CategoryName, it does not include any fields from Order_Details or Product. This behavior is very important to our application for performance reasons. Selecting all fields when we don't need them can have a significant impact on query performance.
There does not seem to be a way to accomplish the same in OData V4. The equivalent query returns all fields from Order_Details and Product
http://services.odata.org/V4/Northwind/Northwind.svc/Order_Details(OrderID=10248,ProductID=11)?$expand=Product($expand=Category($select=CategoryName))
The closest I can get is to just select one field from each level, introduces a lot of complexity into our code, and it has been difficult to ensure that all queries (future and existing) adhere to this rule.
The closest I can get is to just select one field from each level,
introduces a lot of complexity into our code, and it has been
difficult to ensure that all queries (future and existing) adhere to
this rule.
Looks something like this:
http://services.odata.org/V4/Northwind/Northwind.svc/Order_Details(OrderID=10248,ProductID=11)?$expand=Product($select=Category;$expand=Category($select=CategoryName))&$select=Product
There is certainly a bit of added complexity here, but this was acceptable in my case.
The syntax for this is:
https://services.odata.org/V4/Northwind/Northwind.svc/Order_Details(OrderID=10248,ProductID=11)/Product/Category?$select=CategoryName
results in:
{
"#odata.context": "https://services.odata.org/V4/Northwind/Northwind.svc/$metadata#Categories(CategoryName)/$entity",
"CategoryName": "Dairy Products"
}
The key OData v4 concept here is that the path, that is everything before the ? defines the resource that is being served, and by that the shape of the resulting graph. The output of $select and $expand (projections) is constrained to match the requested resource.
So in v3 with $select you could return a more arbitrary structure but in v4 the $select and $expand can only mask the graph by returning what is essentially a subset of $select=*&$expand=*.
To get around this but still allow similar query scenarios in v4 we can compose an entity path expression to *any resource within the parent path.
So we move the resource selector path from the v3 $select Product/Cateogry' and append it the path of our resource ~/Order_Details(OrderID=10248,ProductID=11)`
NOTE: There is a strong caveat to this, whilst the OData specification describes this behaviour, not all implementations support deep resource selections like this. The specification is a guidance document on the standard protocol, not all implemenations are 100% compliant.
A simplification of this is to try selecting just the Product from the same query, notice here we do not use any query parameters at all:
~/Order_Details(OrderID=10248,ProductID=11)/Product
{
"#odata.context": "https://services.odata.org/V4/Northwind/Northwind.svc/$metadata#Products/$entity",
"ProductID": 11,
"ProductName": "Queso Cabrales",
"SupplierID": 5,
"CategoryID": 4,
"QuantityPerUnit": "1 kg pkg.",
"UnitPrice": 21.0000,
"UnitsInStock": 22,
"UnitsOnOrder": 30,
"ReorderLevel": 30,
"Discontinued": false
}
You can see in this response that the context or the resource that is being returned is a $metadata#Products/$entity and not an Order_Details/$entity
Once your resource is selected, normal v4 $select and $expand logic is evaluated. This is documented in the specification under 4.3 Addressing Entities
These rules are recursive, so it is possible to address a single entity via another single entity, a collection via a single entity and even a collection via a collection; examples include, but are not limited to:
By following a navigation from a single entity to another related entity (see rule: entityNavigationProperty)
Example 14:
http://host/service/Products(1)/Supplier
Update:
I've substantially edited this post from my original answer, at the time i misinterpreted OP's request and the structure they were expecting, but this is still relevant information in 2022 and none of the answers directly produces the desired behaviour.
The simplest solutions would be to create View with required schema on your db server and try to fetch data from this datasource with filters and column name(s) instead.
Especially when facing issues with performance.
The best way would be to register this class to your IoC as singleton
public class InternalODataEdmModelBuilder
{
private readonly ODataConventionModelBuilder _oDataConventionModelBuilder = new ODataConventionModelBuilder();
private IEdmModel _edmModel;
public InternalODataEdmModelBuilder()
{
ODataEntitySetsConfigInternal.Register(_oDataConventionModelBuilder);
}
// cache
public IEdmModel GetEdmModel()
{
return _edmModel ?? (_edmModel = _oDataConventionModelBuilder.GetEdmModel());
}
}
internal static class ODataEntitySetsConfigInternal
{
public static void Register(ODataConventionModelBuilder oDataModelBuilder)
{
if (oDataModelBuilder == null)
{
throw new Exception("'ODataConventionModelBuilderWebApi' cannot be null");
}
oDataModelBuilder.EntitySet<YourView>("YourView").EntityType.HasKey(x => x.YourKey);
}
}
And then inject this registered object in your API controller and build your query from URL like this:
ODataQueryContext queryContext = new ODataQueryContext(_oDataConventionModel, typeof(YourViewEFType), null);
var option = new ODataQueryOptions(queryContext, Request);
var data = option.ApplyTo(data, new ODataQuerySettings { EnsureStableOrdering = false });
And then map data into your POCO (API EDM model shown to the public world).
Hope this helps.
I have a doubt about what could be the best way to define a REST URI for an API.
I have an API that provide the details of a commodity.
So I know that I can do a GET request like this:
http://XXX.YYY.ZZZ.TTT:8280/commodity_details/1
where commodity_details is what I want to obtain (a commodity details) and 1 is the ID of a specific commodity. This should be a proper REST URI.
Ok, I know that I can also pass the ID parameter into a JSON document doint a POST request like this:
http://XXX.YYY.ZZZ.TTT:8280/commodity_details/
and attacching a JSON payload like this to my POST request:
{
"commodity_id": 1
}
I think that if I have the single commodity_id parameter maybe is better the first version (putting the required ID into the URI), is it?
But what happens if I need a second language_id parameter? (my API should receive also this language_id parameters so it can provide an internazionalized output in the proper language.
So in this case I need to pass 2 parameters (commodity_id and language_id).
In this case is better use a POST request with a JSON payload that contains both the parameters? Something like this:
{
"commodity_id": 1,
"language_id": 2
}
Or what could be a good URI template for this scenario?
For passing just 2 parameters you can go with first approach(query string parameter) which is simpler to use
[HttpGet("{commodity_id}/{language_id}")]
public string GetCommodityDetails(string commodity_id, string language_id)
{
string commodityDetails=string.empty;
//your implementation
return commodityDetails;
}
Is it possible to implement the get operation with filter parameters as below:
public IHttpResult Get([FromODataUri] int productId, int accountId)
{
...
}
Note that we are not supposed to use [ODataRoute] attribute to customize it. Any thoughts?
Get operation with multiple parameters works just fine as usual, just need to call the service with named query string parameters. For example for the above Get operation the URL can be: http://localhost/myapp/odata/Products/?productId=1&accountId=2
I've an entity with an ID of
public string ID {get;set;}
activities/1
(which comes from RavenDB).
I'm registering the following routes in my ServiceStack AppHost
Routes
.Add<Activity>("/activities")
.Add<Activity("/activities/{id}");
I'm using a backbone app to POST and PUT to my REST Service.
What happens out-of-the-box:
id property is serialized into the json as "activities/1"
id property is encoded into route as "activities%2F1"
ServiceStack gives precedence to the URL based id property, so my string gets the encoded value which is no use to RavenDb directly.
The options I'm aware of:
Change backbone to post to "/activities" and let the JSON Serialiser kick in
Change RavenDb ID generation to use hyphens rather than slashes
Make my Id property parse for the encoded %2F on set and convert to a slash
Both have disadvantages in that I either lose RESTfulness in my API, which is undesirable, or I don't follow RavenDb conventions, which are usually sensible out-of-the-fox. Also, I've a personal preference for having slashes.
So I'm wondering if there are any other options in servicestack that I could use to sort this issue that involve less compromise? Either Serialiser customisation or wildcard routing are in my head....
I have the same problem with ASP.Net WebAPI, so I don't think this is so much a ServiceStack issue, but just a general concern with dealing with Raven style id's on a REST URL.
For example, let's say I query GET: /api/users and return a result like:
[{
Id:"users/1",
Name:"John"
},
{
Id:"users/2",
Name:"Mary"
}]
Now I want to get a specific user. If I follow pure REST approach, the Id would be gathered from this document, and then I would pass it in the id part of the url. The problem here is that this ends up looking like GET: /api/users/users/1 which is not just confusing, but the slash gets in the way of how WebAPI (and ServiceStack) route url parameters to action methods.
The compromise I made was to treat the id as an integer from the URL's perspective only. So the client calls GET: /api/users/1, and I define my method as public User Get(int id).
The cool part is that Raven's session.Load(id) has overloads that take either the full string form, or the integer form, so you don't have to translate most of the time.
If you DO find yourself needing to translate the id, you can use this extension method:
public static string GetStringIdFor<T>(this IDocumentSession session, int id)
{
var c = session.Advanced.DocumentStore.Conventions;
return c.FindFullDocumentKeyFromNonStringIdentifier(id, typeof (T), false);
}
Calling it is simple as session.GetStringIdFor<User>(id). I usually only have to translate manually if I'm doing something with the id other than immediately loading a document.
I understand that by translating the ids like this, that I'm breaking some REST purist conventions, but I think this is reasonable given the circumstances. I'd be interested in any alternative approaches anyone comes up with.
I had this problem when trying out Durandal JS with RavenDB.
My workaround was to change the URL very slightly to get it to work. So in your example:
GET /api/users/users/1
Became
GET /api/users/?id=users/1
From jQuery, this becomes:
var vm = {};
vm.users = [];
$.get("/api/users/?" + $.param( { id: "users/1" })
.done(function(data) {
vm.users = data;
});