The situation
I'm trying to expand "Item" to three levels:
Item.Product.Model.Type
So I call this nested query options url:
http://xxx/api/Items?$expand=Product($expand=Model($expand=Type))
I Get a warning that the max depth of 2 has been reached so I set the suggested MaxExpansionDepth attribute to 3. But then, the "Type" property is not returned! This is covered by this SO question
Then I look at the official OData V4 standard and it says I should use slashes in the expand option, like so:
http://xxx/api/Items?$expand=Product/Model/Type
But that gives me an error telling:
The query specified in the URI is not valid. Found a path traversing multiple navigation properties. Please rephrase the query such that each expand path contains only type segments and navigation properties.
Which this SO answer covers but the answer is contradictory to the official OData doc. What does that even mean anyway.
The question
What is the official, standard and working way of using the $expand query option for deep levels with OData v4 and Web API 2.2
After working with Jerther in chat, we narrowed the problem down to the expanded properties not being marked as contained navigations. As a result, the OData framework was removing them since they did not have corresponding entity sets defined. Updating the model to specifically declare the containment appears to have solved the problem.
Containment can be specified in a couple of ways, depending on what model builder is being used. In the case of the ODataConventionModelBuilder you can add the System.Web.OData.Builder.ContainedAttribute to the property in question, while for the ODataModelBuilder you can use the ContainsMany<T> method on the EntityTypeConfiguration instance for the containing class.
Also, at the moment, a cascaded expand will stop where a complex type contains an Entity type.
UPDATE:
Defining all types in the chain as EntitySet works.
builder.EntitySet<Item>("Items");
builder.EntitySet<Product>("Products");
builder.EntitySet<Model>("Models");
builder.EntitySet<Type>("Types");
It seems defining them as EntityType isn't sufficient.
see here: https://github.com/OData/WebApi/issues/226
Original Answer
I tried reproing your situation and couldn't. Is it possible that "Types" isn't being set in your action? Here was my little repro
public class ItemsController : ODataController
{
[HttpGet]
[EnableQuery(MaxExpansionDepth = 10)]
[ODataRoute("/Items")]
public IHttpActionResult GetItems()
{
return this.Ok(CreateItem());
}
private Item CreateItem()
{
return new Item
{
Id = 1,
Products = new Product[]
{
new Product
{
Id = 2,
Models = new Model[]
{
new Model
{
Id = 3,
Types = new MyType[]
{
new MyType
{
Id = 4,
},
},
},
},
},
},
};
}
}
Which when called with /Items?$expand=Products($expand=Models($expand=Types)) resulted in the following:
{
"#odata.context": "http://localhost:9001/$metadata#Items/$entity",
"Id": 1,
"Products#odata.context": "http://localhost:9001/$metadata#Items(1)/Products",
"Products": [{
"Id": 2,
"Models#odata.context": "http://localhost:9001/$metadata#Items(1)/Products(2)/Models",
"Models": [{
"Id": 3,
"Types#odata.context": "http://localhost:9001/$metadata#Items(1)/Products(2)/Models(3)/Types",
"Types": [{
"Id": 4
}]
}]
}]
}
Related
I've been trying to update to ES6 and NEST 6 and running into issues with NEST serializing of search requests - specifically serializing Terms queries where the underlying C# type is an enum.
I've got a Status enum mapped in my index as a Keyword, and correctly being stored in its string representation by using NEST.JsonNetSerializer and setting the contract json converter as per Elasticsearch / NEST 6 - storing enums as string
The issue comes when trying to search based on this Status enum. When I try to use a Terms query to specify multiple values, these values are being serialized as integers in the request and causing the search to find no results due to the type mismatch.
Interestingly the enum is serialized correctly as a string in a Term query, so I'm theorizing that the StringEnumConverter is being ignored in a scenario where it's having to serialize a collection of enums rather than a single enum.
Lets show it a little more clearly in code. Here's the enum and the (simplified) model used to define the index:
public enum CampaignStatus
{
Active = 0,
Sold = 1,
Withdrawn = 2
}
public class SalesCampaignSearchModel
{
[Keyword]
public Guid Id { get; set; }
[Keyword(DocValues = true)]
public CampaignStatus CampaignStatus { get; set; }
}
Here's a snippet of constructing the settings for the ElasticClient:
var pool = new SingleNodeConnectionPool(new Uri(nodeUri));
var connectionSettings = new ConnectionSettings(pool, (builtin, serializerSettings) =>
new JsonNetSerializer(builtin,
serializerSettings,
contractJsonConverters: new JsonConverter[]{new StringEnumConverter()}
)
)
.EnableHttpCompression();
Here's the Term query that correctly returns results:
var singleTermFilterQuery = new SearchDescriptor<SalesCampaignSearchModel>()
.Query(x => x.Term(y => y.Field(z => z.CampaignStatus).Value(CampaignStatus.Active)));
Generating the request:
{
"query": {
"term": {
"campaignStatus": {
"value": "Active"
}
}
}
}
Here's the Terms query that does not return results:
var termsFilterQuery = new SearchDescriptor<SalesCampaignSearchModel>()
.Query(x => x.Terms(y => y.Field(z => z.CampaignStatus).Terms(CampaignStatus.Active, CampaignStatus.Sold)));
Generating the request:
{
"query": {
"terms": {
"campaignStatus": [
0,
1
]
}
}
}
So far I've had a pretty good poke around at the options being presented by the JsonNetSerializer, tried a bunch of the available attributes (NEST.StringEnumAttribute, [JsonConverter(typeof(StringEnumConverter))] rather than using the global one on the client, having an explicit filter object with ItemConverterType set on the collection of CampaignStatuses, etc.) and the only thing that has had any success was a very brute-force .ToString() every time I need to query on an enum.
These are toy examples from a reasonably extensive codebase that I'm trying to migrate across to NEST 6, so what I'm wanting is to be able to specify global configuration somewhere rather than multiple developer teams needing to be mindful of this kind of eccentricity.
So yeah... I've been looking at this for a couple of days now. Good chances there's something silly I've missed. Otherwise I'm wondering if I need to be providing some JsonConverter with a contract that would match to an arbitrary collection of enums, and whether NEST and their tweaked Json.NET serializer should just be doing that kind of recursive resolution out of the box already.
Any help would be greatly appreciated, as I'm going a bit crazy with this one.
I have the following controller method:
#RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, value = "session/{id}/exercises")
public ResponseEntity<Resources<Exercise>> exercises(#PathVariable("id") Long id) {
Optional<Session> opt = sessionRepository.findWithExercises(id);
Set<Exercise> exercises = Sets.newLinkedHashSet();
if (opt.isPresent()) {
exercises.addAll(opt.get().getExercises());
}
Link link = entityLinks.linkFor(Session.class)
.slash(id)
.slash(Constants.Rels.EXERCISES)
.withSelfRel();
return ResponseEntity.ok(new Resources<>(exercises, link));
}
So basically I am trying to get the expose a Set<> of Exercise entities for a particular Session. When the exercises entity is empty however I get a JSON representation like this:
{
"_links": {
"self": {
"href": "http://localhost:8080/api/sessions/2/exercises"
}
}
}
So basically there is no embedded entity, while something like the following would be preferrable:
{
"_links": {
"self": {
"href": "http://localhost:8080/api/sessions/2/exercises"
}
},
"_embedded": {
"exercises": []
}
}
any idea how to enforce this?
The problem here is that without additional effort there's no way to find out that the empty collection is a collection for Exercise. Spring HATEOAS has a helper class to work around this though:
EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(Exercise.class);
Resources<Object> resources = new Resources<>(Arrays.asList(wrapper));
An EmbeddedWrapper allows you to explicitly mark objects to be added to the Resource or Resources as embedded, potentially even manually defining the rel they should be exposed under. As you can see above the helper also allows you to add an empty collection of a given type to the _embedded clause.
One can use the PagedResourceAssembler::toEmptyResource() method. For example, the following works:
Page<EWebProduct> products = elasticSearchTemplate.queryForPage(query, EWebProduct.class);
if(!products.hasContent()){
PagedResources pagedResources = pageAssembler.toEmptyResource(products, WebProductResource.class,baseLink);
return new ResponseEntity<PagedResources<WebProductResource>>(pagedResources, HttpStatus.OK);
}
I'd bet it works with other ResourceAssemblers as well.
If you have a Page< T >, you can convert it like this:
public static <T> PagedModel<EntityModel<T>> toModel(PagedResourcesAssembler<T> assembler,
Page<T> page) {
if (!page.isEmpty()) {
return assembler.toModel(page);
} else {
// toEmptyModel renders the _embedded field (with an empty array inside)
return (PagedModel<EntityModel<T>>) assembler.toEmptyModel(page, TenantSubscriptionResponseDto.class);
}
}
(You can obtain the PagedResourcesAssembler assembler by simply adding it as a parameter to the Controller method, and Spring will inject it).
Spring by default uses Jackson parser to serialize/deserialize json. As per http://wiki.fasterxml.com/JacksonFeaturesSerialization Jackson has a feature called WRITE_EMPTY_JSON_ARRAYS and its enabled by default. Maybe WRITE_EMPTY_JSON_ARRAYS is set to false in your config. please recheck your message converters configuration.
Consider the following method in a Web Api controller:
[Queryable(AllowedQueryOptions= AllowedQueryOptions.All)]
public override IQueryable<Mandate> Get()
{
return new List<Mandate>() { new Mandate() {
Id = 1,
PolicyNumber = "350000000",
OpenPositions = new List<OpenPosition>(){
new OpenPosition(){ Id = 1, Amount =2300 },
new OpenPosition(){ Id = 2, Amount =2100 }
}},
new Mandate() {
Id = 2,
PolicyNumber = "240000000" ,
OpenPositions = new List<OpenPosition>(){
new OpenPosition(){ Id = 3, Amount =2500 },
new OpenPosition(){ Id = 2, Amount =2100 }
}
} }.AsQueryable<Mandate>();
}
Here the list is built manually and if I browse to the following url:
http://localhost:52446/odata/Mandates?$filter=Id eq 1 it returns the correct item from the list.
Now obviously the list is more likely to be a database structure. Data would be retrieved using some ORM and returned to the Web API service.
I don't use Entity Framework (and I can't because of legacy systems).
How would I use Web API in this case? How would I translate the url parameters so that the filters are applied by the layer responsible of the data access?
Got it. You pointed me into the right direction with your LINQ provider. I found out I can do it easily with the ORM we are using (OpenAccess). More info here :http://docs.telerik.com/data-access/developers-guide/using-web-services/asp.net-web-api/developer-guide-wcfservices-web-api-expose-oacontext
I have found plenty of posts about this, but none of them are solving my issue. My code right now:
#Html.ListBox("SelectedNewsletter", Model.Newsletters)
and
public MultiSelectList Newsletters
{
get
{
return new MultiSelectList(
new[]
{
// TODO: Fetch from your repository
new { Id = 1, Name = "item 1" },
new { Id = 2, Name = "item 2" },
new { Id = 3, Name = "item 3" },
},
"Id",
"Name"
);
// return new MultiSelectList(PromoNewsletter.All, "IdString", "Display");
}
}
As far as I can see, it's all hard coded now, and it still gives the same error. I want to do a ListboxFor, but I am trying to just get a listbox to work. I have replaced my int id with a string representation, based on advice I found elsewhere, but now I don't see what else I can do. It just plain is not working, even with all hard coded values and not binding to a property on my ViewModel. Where am I going wrong ?
The error is occurring because you have a property in the Model or ViewData/ViewBag with the name SelectedNewsletter.
Give a different name for the ListBox if you can't rename that property.
UPDATE:
After little more experimenting I figured out that the problem is you may be setting an integer value to the SelectedNewsletter that is somewhere in the ViewData/ViewBag or Model.
You can set the values that has to be selected in the ListBox as a string or array of strings to the SelectedNewsletter.
i.e SelectedNewsletter = "1";
or
SelectedNewsletter = new[] { "1", "3" };
You can also use strongly typed helper to make things easy,
#Html.ListBoxFor(m => m.SelectedNewsletter, Model.NewsLetters);
I have two documents that looks a bit like so:
Doc
{
_id: AAA,
creator_id: ...,
data: ...
}
DataKey
{
_id: ...,
credits_left: 500,
times_used: 0,
data_id: AAA
}
What I want to do is create a view which would allow me to pass the DataKey id (key=DataKey _id) and get both the information of the DataKey and the Doc.
My attempt:
I first tried embedding the DataKey inside the Doc and used a map function like so:
function (doc)
{
if (doc.type == "Doc")
{
var ids = [];
for (var i in doc.keys)
ids.push(doc.keys[i]._id);
emit(ids, doc);
}
}
But i ran into two problems:
There can be multiple DataKey's per
Doc so using startkey=[idhere...]
and endkey=[idhere..., {}] didn't
work (only worked if the key happend
to be the first one in the array).
All the data keys need to be unique, and I would prefer not making a seperate document like {_id = datakey} to reserve the key.
Does anyone have ideas how I can accomplish this? Let me know if anything is unclear.
-----EDIT-----
I forgot to mention that in my application I do not know what the Doc ID is, so I need to be able to search on the DataKey's ID.
I think what you want is
function (doc)
{
if (doc.type == "Doc")
{
emit([doc._id, 0], doc);
}
if(doc.type == "DataKey")
{
emit([doc.data_id, 1], doc);
}
}
Now, query the view with key=["AAA"] and you will see a list of all docs. The first one will be the real "Doc" document. All the rest will be "DataKey" documents which reference the first doc.
This is a common technique, called CouchDB view collation.