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.
Update: Thankyou for updating the post with the platform/vendor version, that changes everything, you are not asking about standards anymore but about a specific implementation, which is the correct approach.
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:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=Document&
$orderby=Document/FileInfo/Filename&
$expand=Document($select=FileInfo/Filename)
NOTE: Some server-side implementations or constraints might require that certain fields are returned, even if you do not request them. This is a server-side configuration and is outside of the scope of the specifications. The specs do not specifically state that non-requested fields cannot be returned, only that the requested fields MUST be included in the response. Custom implementations are allowed to return additional properties as long as they are declared correctly in the $metadata then they will be supported.
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 single Document property that also has a single FileInfo that has a single Filename property.
So from a pure OData v4 specification point of view, what you are asking for is totally against the core principle of OData (v4), read on for additional variations and exceptions to this rule...
RE: 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:
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 a Response 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.
{
"#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"
}
}
}
]
}
If you are expecting a simple OData array of strings like the following, then you will have to write some extra code:
{
"value": [
"BST-abc-dd100-04.pdf",
"BST-abc-dd100-04.pdf",
"BST-DEF-DD100-01.PDF"
]
}
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:
[
"BST-abc-dd100-04.pdf",
"BST-abc-dd100-04.pdf",
"BST-DEF-DD100-01.PDF"
]
Previous versions of OData did support direct querying of child resources, but in v4 specification this is only supported by Entity navigation links
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:
https://localhost:12345/TestApp/Requests?
$count=true&
$select=File&
$orderby=File&
$compute=Document/FileInfo/Filename as File
Should result in something similar to this:
{"#odata.context":"...",
"#odata.count":3,"value":
[{"File":"test1.pdf"},
{"File":"test2.pdf"},
{"File":"test3.PDF"}
]}
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:
[HttpGet]
[EnableQuery]
public async Task<IActionResult> Filenames()
{
IQueryable<Request> query = GetRequestsQuery();
return Ok(query.Select(x => x.Document.FileInfo.Filename).ToArray());
}
...
builder.EntitySet<Request>("Requests").EntityType.Collection.Function(nameof(Filenames)).ReturnsCollection<string>();
Then you could query this via the following URL:
https://localhost:12345/TestApp/Requests/Filenames
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 the Filename 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.
I get data from my database (Firebase database). This is a document with several data, including a Array with GeoPoint fields.
var documents = await CrossCloudFirestore.Current .Instance .Collection("Activity") .GetAsync();
My object is defined like this in my Model class: public class Activity { public object elapsedTime { get; set; } public object positions { get; set; } ...... }
It works. I can have the data on my Activity class.
However, I would like to know how I can access this data because foreach loops do not work for object types. Maybe I need to redefine my Activity Class ?
Thx :=)
Edit :
You can see here my firebase :
And then the result of the request :
In your scenario , the property is dynamic for each object , I would suggest you deserialize the root level as a dictionaryand then get the data you want .
Items = JsonConvert.DeserializeObject<Dictionary<string, YourClass>>(content).Values.ToList()
Oh before this, Newtonsoft plugin is required .
Refer to
Deserialize JSON into readable format
It works ! :)
To do it, I defined Array (from firestore) as IEnumerable on my C# class, with the same name from firestore to c#.
And then, here is my query and the line to get my objects converted.
I got the requirement that at one API endpoint should be possible to post a json which can look differently.
1.)
"payload": {
"someJSON" : "something"
}
2.)
"payload":
"Hier könnte irgendwelcher Text stehen."
I tried to set Payload as a type object, but as soon I put some thing in the payload value the Payload object is not valid anymore, it is null than.
[JsonProperty("payload", Required = Required.AllowNull)]
public object Payload { get; set; }
How it could be possible to deal with this requirement of different types in one object?
I have a one to many relationship as outlined below. In some parts of the business layer there are queries of the Item table and in others the Client table (as well as its Items). LazyLoading and ProxyCreation are both false, reference loop handling is set to ignore.
public class Client {
public virtual ICollection<Item> Items { get; set; }
public string Name {get;set;}
}
public class Item {
public virtual Client TheClient {get;set;}
public string ItemProp {get;set;}
// another 10 properties or so
}
myitems = dbContextScopeX.Items.Include(x => x.Client).ToList();
The view has a list of items with the need to show the Client's Name (in my example). I am looking for item.Client.Name ultimate, however when myitems gets queries/serialized it contains:
myitems.Client.Items
If I set the attribute [JsonIgnore] on the Client's Item property it never comes through the graph which I need it to in other places. Is there a way to get myItems.Client.Name without having to get myitems.Client.Items in the query or without having to create an anonymous projection for the Item array?
Project the Item properties you want (be they simple or complex type) along with just the Client name into an anonymous type and serialize that.
myitems = dbContextScopeX.Items.Include(x => x.Client)
.Select(i=>new {
ItemProp = i.ItemProp,
ItemCollection = i.ItemCollection,
...
ClientName = i.Client.Name
}).ToList();
Only caveat is you have to do some manual work if you want to deserialize this back into entities.
In my one of the view I add a dropdown and I bind this drop down with my database like this..
public ActionResult PostMarks()
{
JoinMod std = new JoinMod();
std.Drp_bind = (from nm in db.TbStudent
select new SelectListItem
{
Text = nm.StudentName,
Value = SqlFunctions.StringConvert((double)nm.StudentId).Trim()
}).ToList<SelectListItem>();
return View(std);
}
Here is the dropdownlist in my view
<p>StudentName</p>
#Html.DropDownList("StudentName",Model.Drp_bind,"Select")
And on Post I am trying to save the data into the database like this
[HttpPost]
public ActionResult PostMarks(Marks marks)
{
db.TbMarks.Add(marks);
db.SaveChanges();
return RedirectToAction("ShowAllMarks");
}
Now when I check my database after save data in database the Id save in the database is zero from the dropdownlist. Please experts help me to solve this issue
You should be using #HTML.DropDownListFor, specifying a lambda that indicates which property in your model you want to bind the list's selected value to on the POST.
Given a view model like so:
public class MarksViewModel
{
public Marks Marks { get; set; }
public IEnumerable<SelectListItem> Drp_bind { get; set; }
}
The drop down list in the CSHTML can be declared like so:
#Html.DropDownListFor(m => m.Marks.StudentId, m.Drp_bind, "Select a Student")
The first argument is an expression describing the property on your model that you want to populate with the selected value from the drop down list in the post back. The second is the list comprising the values to which we need to data bind.
Note that your CSHTML will need more form fields within it bound to the other properties of the Marks property.
Your POST method would now take MarksViewModel as an argument and extract the Marks property from it to add to the DbContext for saving.