I want to retrieve a single property from a complex data type.
Platform: EF Core 6, OData V4, Blazor on Windows 11, VS 2022 on a MS SQL Express database.
Simplified DB / entity structure:
[Owned]
public class FileInfo
{
[StringLength(255)]
public string Filename { get; set };
}
public class UserInfo
{
[StringLength(80)]
public string UserID { get; set; }
[StringLength(200)]
public string Name { get; set; }
[StringLength(200)]
public string Email { get; set; }
}
public class Document
{
[Key]
public Guid DocumentID { get; set; }
public FileInfo FileInfo { get; set; }
[StringLength(80)]
public string OwnerID { get; set; }
public virtual UserInfo? Owner { get; set; }
}
public class Request
{
[Key]
public Guid RequestID { get; set; }
[StringLength(80)]
public string AuthorID { get; set; }
[ForeignKey("AuthorID")]
public virtual UserInfo? Author;
public Guid DocumentID { get; set; }
[ForeignKey("DocumentID")]
public virtual Document? Document;
}
Entities etc.:
public static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Document>("Documents");
modelBuilder.EntitySet<Request>("Requests");
modelBuilder.ComplexType<FileInfo>();
return modelBuilder.GetEdmModel();
}
Query #1:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=Document&
$orderby=Document/FileInfo/Filename&
$expand=Document($select=FileInfo/Filename)
This query returns:
{"@odata.context":"https://localhost:44393/DocServer2/
$metadata#Requests(Document,Document(FileInfo/Filename))",
"@ odata.count":3,"value":[
{"Document":{"FileInfo":{"Filename":"BST-abc-dd100-04.pdf"}}},
{"Document":{"FileInfo":{"Filename":"BST-abc-dd100-04.pdf"}}},
{"Document":{"FileInfo":{"Filename":"BST-DEF-DD100-01.PDF"}}}]}
However, I actually only need a list of strings (the property values).
This is not all though. Things are getting ugly when I apply a filter to the query, requiring me to look at and hence expand more data:
https://localhost:12345/TestApp/Requests?
$count=true&$orderby=Document/FileInfo/Filename&
$select=Document&
$filter=(Document/OwnerID eq 'Testuser') or (AuthorID eq 'TestUser')&
$expand=Author,Document($expand=Owner;$select=FileInfo/Filename)
The result looks like this:
{"@odata.context":"https://localhost:12345/TestApp/
$metadata#Requests(
Document,Author(),
Document(FileInfo/Filename,
Owner()))",
"@odata.count":1,
"value":[
{"Author":{"Name":"Test User"},
"Document":{"FileInfo":{"Filename":"Test.PDF"},
"Owner":{"Name":"Test User"}}}]}
Note: Using "$select=" instead of "$select=Document" returns all property values of Document (seems to be treated like "select * from Documents").
How do I need to adjust the query to only return Request.Document.FileInfo.Filename?
I did google and also searched SO for an answer, but couldn't find one.
You are correct that to
$select
a specific property on a ComplexType you should use the/
to address it as a descendant of the name of the root property:Unfortunately for your case a key tenant of OData V4 over other APIs is that the structure of the resource will not change. Entities are resources (the R in REST) and OOTB in the .Net implementations this cannot be violated. This means that the response will always be an array of
Request
objects that have a singleDocument
property that also has a singleFileInfo
that has a singleFilename
property.There is no reason that you need to include other
$expand
or$select
properties to evaluate a$filter
.$filter
is evaluated first and independently from (and before) the$select
and$expand
. So you are not required to include these properties in your request at all, but if you do include those navigation pathways in the request, it makes sense that those fields and/or navigation properties would be included in the response.If you query the
Requests
controller, then according to the OData specification, the response should be in the shape of aResponse
object. We can used$select
and$expand
to reduce the bytes transferred over the wire by omitting properties, but relationship structure or general shape of the object graph MUST be maintained to allow the client-side implementations to work correctly.If you are expecting a simple OData array of strings like the following, then you will have to write some extra code:
or perhaps, if you want a pure custom REST/JSON response you can do that too, but it's not conformant to the OData specification anymore:
You OData controllers are just a great start for whatever you want to add to your OData implementation. If you have a genuine need to return a flattened list, then you can add an additional function to your controller to support this,
There is a feature described in the OData 4.01 amended specification that does allow you to use an alias to reference the result of a
$compute
query option. However, this was not included in the specification until 2020, not many older implementations are likely to have support for this new option, EF Core (Microsoft.AspNetCore.OData v8.0.12) only has partial support for this syntax.It is expected to work like this:
Should result in something similar to this:
Unfortunately as I test this I encounter a bug in Microsoft.AspNetCore.OData v8.0.12 that does not allow you to
$select
the aliased column, you can see the column included if you use$select=*
but I cannot scope the response to just that column.Please try it on your API to confirm, but until $compute works if you have need of a specific shape of data, then you should add a function or action endpoint to return that desired data. OData is just a tool to help you get there, using OData does not preclude you from adding custom endpoints, as long as you define them correctly, they will still be exposed through the metadata and can be easily consumed by clients that implement code generators.
To implement a custom function to retrieve this data, you can use a controller method similar to this:
Then you could query this via the following URL:
However OData Query Options can only be enforced on the response type of the method, so even with
[EnableQuery]
OOTB you can only$filter
or$orderby
the values in theFilename
property.There are other workarounds, including Open Type support, but if you are interested in the
$compute
solution but cannot get it to work, then we should raise an issue with https://github.com/OData/odata.net/issues?q=compute to get the wider community involved.