OData select with complex data type - select

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.

Related

EF Lazy load proxy interceptor

I'm looking for a way to intercept Entity Framework's lazy load proxy implementation, or otherwise control what is returned when accessing a Navigation property that may have no value in the database.
An example of what I'm looking for is this Contact class with mailing address, business phone, etc. that may or may not have a contact person.
public partial class Contact
{
private Nullable<System.Guid> _personId;
public Nullable<System.Guid> PersonId
{
get { return _personId; }
set { SetProperty(ref _personId, value); }
}
public virtual Person Person{ get; set; }
// mailing address, other properties...
}
public partial class Person
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
public string LastName
{
get { return _lastName;}
set { SetProperty(ref _lastName;value); }
}
}
It is very useful in ASP.net Razor pages, WPF or ad-hoc reporting tools, to be able to use expressions like:
Contact c = repo.GetContact(id);
Console.WriteLine("Contact Person " + c.Person.FirstName);
Which of course fails if there is no PersonId, and hence contact.Person is null.
Tools like Ideablade Devforce have a mechanism to return a "NullEntity" for Person in this case, which allows the WriteLine to succeed. Additionally, the NullEntity for Person can be configured to have a sensible value for FirstName, like "NA".
Is there some way to override the Dynamic Proxy mechanism in EF, or otherwise intercept the reference to Person from Contact to enable this scenario?
I have investigated IDbCommandInterceptor, but that does not seem to intercept virtual navigation to individual entity properties, only navigation to entity collections.
Update _____________________________________
To elaborate on my original question, I can't modify the expression by introducing null conditional operators into the them, as these expressions are incorporated into WPF, ASP.Net Razor binding expressions, and/or report data fields, created by other developers or authors. Also, there may be multiple layers of null properties to deal with, e.g. Contact.Person.Spouse.FirstName, where either Person and/or Spouse might be a "null" property. The Devforce Ideablade implementation deals with this perfectly, but is unfortunately not an option on my current project.
you can use a null-conditional operator from c# like this
c.Person?.FirstName
This means that when Person == null , return null or otherwise return FirstName. You would still need to handle the null value
See : https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-

Using REST to try and get Field Service Detail - InventoryID = SQL error: Multi-part identifier not found

I am getting SQL errors when trying to use REST to get to FSAppointmentDet.InventoryID, either as a Field Service service item or as an Inventory Item.
The InventoryID field exists in the table, however, it looks like the DACs have been inherited, for example as FSAppointmentDetService.
Other fields work, it just seems that the fields with an ID are causing the SQL error.
In this case, the SQL error is a multi-step identifier not found. Running a SQL Profiler trace and looking at the SQL, it looks like the table has been aliased in one part of the query and not in another. Obviously this is occurring at a level much lower than we can get to, so looking for a workaround or ideas on how to get the InventoryID for Field Service detail records.
I've seen this happen when one DAC herits (herits as in class inheritance not extend as in DAC extension) from another DAC without redeclaring it's key fields. The way to fix that is to add the parent keys abstract class fields in the children.
FSAppointmentDetService seems to be missing AppointmentID key declaration. When the ORM builds the SQL query it generates Alias for the herited DAC but it gets confused becaused the key fields of the parent were not all re-declared in the child.
In FSAppointmentDet you have 2 key fields:
#region AppointmentID
public abstract class appointmentID : PX.Data.IBqlField
{
}
[PXDBInt(IsKey = true)]
[PXParent(typeof(Select<FSAppointment, Where<FSAppointment.appointmentID, Equal<Current<FSAppointmentDet.appointmentID>>>>))]
[PXDBLiteDefault(typeof(FSAppointment.appointmentID))]
[PXUIField(DisplayName = "Appointment Nbr.")]
public virtual int? AppointmentID { get; set; }
#endregion
#region AppDetID
public abstract class appDetID : PX.Data.IBqlField
{
}
[PXDBIdentity(IsKey = true)]
public virtual int? AppDetID { get; set; }
#endregion
But in FSAppointmentDetService only one of them is redeclared. Notice how it's using 'override' to redeclare compared to FSAppointmentDet which do not override:
#region AppDetID
public new abstract class appDetID : PX.Data.IBqlField
{
}
[PXDBIdentity(IsKey = true)]
public override int? AppDetID { get; set; }
#endregion
In this case we can't add field to that DAC though because it's part of the base product. I think it would be possible to create a new DAC that herits from FSAppointmentDetService, add the missing key in there and use that new herited DAC instead of FSAppointmentDetService.
However I don't know if that would be possible when working with Web Services. If not the change will have to be made in Acumatica base product. You could fill a bug report with Acumatica support to have that done in future versions.

When to use Include in EF? Not needed in projection?

I have the following in Entity Framework Core:
public class Book {
public Int32 Id { get; set; }
public String Title { get; set; }
public virtual Theme Theme { get; set; }
}
public class Theme {
public Int32 Id { get; set; }
public String Name { get; set; }
public Byte[] Illustration { get; set; }
public virtual ICollection<Ebook> Ebooks { get; set; }
}
And I have the following linq query:
List<BookModel> books = await context.Books.Select(x =>
new BookModel {
Id = x.Id,
Name = x.Name,
Theme = new ThemeModel {
Id = x.Theme.Id,
Name = x.Theme.Name
}
}).ToListAsync();
I didn't need to include the Theme to make this work, e.g:
List<BookModel> books = await context.Books.Include(x => x.Theme).Select(x => ...
When will I need to use Include in Entity Framework?
UPDATE
I added a column of type Byte[] Illustration in Theme. In my projection I am not including that column so will it be loaded if I use Include? Or is never loaded unless I have it in the projection?
In search for an official answer to your question from Microsoft's side, I found this quote from Diego Vega (part of the Entity Framework and .NET team) made at the aspnet/Announcements github
repository:
A very common issue we see when looking at user LINQ queries is the use of Include() where it is unnecessary and cannot be honored. The typical pattern usually looks something like this:
var pids = context.Orders
.Include(o => o.Product)
.Where(o => o.Product.Name == "Baked Beans")
.Select(o =>o.ProductId)
.ToList();
One might assume that the Include operation here is required because of the reference to the Product navigation property in the Where and Select operations. However, in EF Core, these two things are orthogonal: Include controls which navigation properties are loaded in entities returned in the final results, and our LINQ translator can directly translate expressions involving navigation properties.
You didn't need Include because you were working inside EF context. When you reference Theme inside the anonymous object you are creating, that's not using lazy loading, that's telling EF to do a join.
If you return a list of books and you don't include the themes, then when you try to get the theme you'll notice that it's null. If the EF connection is open and you have lazy loading, it will go to the DB and grab it for you. But, if the connection is not opened, then you have to get it explicitely.
On the other hand, if you use Include, you get the data right away. Under the hood it's gonna do a JOIN to the necessary table and get the data right there.
You can check the SQL query that EF is generating for you and that's gonna make things clearer for you. You'll see only one SQL query.
If you Include a child, it is loaded as part of the original query, which makes it larger.
If you don't Include or reference the child in some other way in the query, the initial resultset is smaller, but each child you later reference will lazy load through a new request to the database.
If you loop through 1000 users in one request and then ask for their 10 photos each, you will make 1001 database requests if you don't Include the child...
Also, lazy loading requires the context hasn't been disposed. Always an unpleasant surprise when you pass an Entity to a view for UI rendering for example.
update
Try this for example and see it fail:
var book = await context.Books.First();
var theme = book.Theme;
Then try this:
var book = await context.Books.Include(b => b.Theme).First();
var theme = book.Theme;

EF Code First validating and updating objects

I am working on an N-tier application consisting of a UI layer (MVC), a Business Layer, a Domain layer (for the models) and a DAL for repositories and the EF DbContext.
I'm a bit confused about the inner workings of Entity Framework when updating the properties of an existing object and I'm looking for a good way to validate an object before updating its values in the database.
I have the following model:
public class BlogPost
{
public int BlogPostId { get; set; }
[Required]
public String Title { get; set; }
[Required]
public String Description { get; set; }
[Required]
public DateTime DateTime { get; set; }
public byte[] Image { get; set; }
}
I have the following methods in my manager in BL:
public BlogPost AddBlogPost(string title, string description, byte[] image = null)
{
BlogPost blogPost = new BlogPost()
{
Title = title,
Description = description,
DateTime = DateTime.Now
};
Validate(blogPost);
moduleRepository.CreateBlogPost(blogPost);
return blogPost;
}
public BlogPost ChangeBlogPost(BlogPost blogPost)
{
moduleRepository.UpdateBlogPost(blogPost);
return blogPost;
}
And I have the following methods in my DAL:
public BlogPost CreateBlogPost(BlogPost b)
{
b = context.BlogPosts.Add(b);
context.SaveChanges();
return b;
}
public BlogPost UpdateBlogPost(BlogPost b)
{
context.Entry(b).State = EntityState.Modified;
context.SaveChanges();
return b;
}
My question now is: what's a good way to check that the model is valid before actually trying to change its values in the database?
I was thinking something like this:
public BlogPost ChangeBlogPost(BlogPost blogPost)
{
// STEP 1: put the updated data in a new object
BlogPost updatedBlogPost = new BlogPost()
{
Title = blogPost.Title,
Description = blogPost.Description,
Image = blogPost.Image,
DateTime = blogPost.DateTime
};
// STEP 2: check if the model is valid
this.Validate(updatedBlogPost);
// STEP 3: read the existing blog post with that ID and change the properties
BlogPost b = moduleRepository.ReadBlogPost(blogPost.BlogPostId);
b.Title = blogPost.Title;
b.Description = blogPost.Description;
b.Image = blogPost.Image;
b.DateTime = blogPost.DateTime;
moduleRepository.UpdateBlogPost(blogPost);
return blogPost;
}
EDIT: I figured it's maybe better to just accept primitive types as parameter in the above method instead of the object.
I have a feeling that's too much work for a simple update, but I couldn't find anything else on the internet.
It's probably also worth noting that I'm using a singleton for the DbContext so I have to make sure Entity Framework doesn't change the values in the database before checking that those values are valid (since another call to the context by another class can cause SaveChanges()).
I know singleton on a DbContext is bad practice, but I saw no other option to avoid countless exceptions when working with multiple repositories and entities being tracked by multiple context instances.
PS: I also read about change tracking in Entity Framework but I'm not 100% sure how this will affect what I'm trying to do.
All suggestions and explanations are welcome.
Thanks in advance.
You would check ModelState.IsValid. There are a lot of validation mechanisms built into MVC that you can take advantage of. Built in attributes such as [Required] that you reference above, custom validators, making your business class implement IValidatableObject, overriding EF SaveChanges() to name a few. This article is a good start: https://msdn.microsoft.com/en-us/data/gg193959.aspx
Ok so I kinda answered my own question while doing some research and testing with some dummy data. I thought that when a property changed in MVC as a result of an Edit view, EF also tracked it and changed it in the database.
I figured out that's not how model binding works and realized after some fooling around that model binding actually creates a new object (instead of editing the properties of a dynamic proxy).
I guess I can now just validate the model and then just update the one with the same primary key in the database.

Returning a subset of a navigation propertie's 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.