-
-
Notifications
You must be signed in to change notification settings - Fork 756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Entity framework core - support for projections, joins, nested filters and sorts #1459
Comments
Currently it is being implemented in #1446, which is told to be released for 10.4. I also have a mostly finished implementation myself at https://github.com/TheJayMann/HotChocolate if you wanted to use it before 10.4 release. I believe it would not require much effort to port once #1446 is complete. |
@TheJayMann Thank you, I didn't know the implementation is in progress. The new feature is just about projections or nested filters/sorts will be implemented as well? |
Filter/sorts is already implemented for a while |
But not nested filters and sorts. Those require integration with selection. Implementing filters at the top level that target a nested property essentially filters top level objects which happen to contain child items which match the filter. Implementing filters at the nested level would filter the child items for each top level object present, and requires integration with selection/projection. Same with sorting. My implementation does have nested filter, though it requires the 11 previews, as it requires a more public access to how the filtering middleware produces IQuerable filter. However, it doesn't have nested sort, as I could not figure out a correct way to implement it. |
@michaelstaib I am aware of that but I meant nested filters/sorts in a manner I posted above (order nested documents by name and take only those with id > 0): {
masters(order_by: {createdAt:DESC}, where: {name_starts_with: "M"}) {
name,
documents(order_by: {name: ASC}, where: {id_gt: 0}) {
createdAt,
id
}
}
} If I include public IQueryable<Master> GetMasters()
{
return dbContext.Masters.Include(x => x.Documents).AsNoTracking();
} join is applied in SQL translation but sorts/filters of nested documents is still missing: SELECT "m"."Id", "m"."CreatedAt", "m"."Name", "d"."Id", "d"."CreatedAt", "d"."MasterId", "d"."Name"
FROM "Masters" AS "m"
LEFT JOIN "Document" AS "d" ON "m"."Id" = "d"."MasterId"
WHERE "m"."Name" LIKE 'M%'
ORDER BY "m"."CreatedAt" DESC, "m"."Id", "d"."Id" |
Nested filters are also already implemented on version 11. if you opt into the preview you can use those. |
Actually I am using the latest preview version |
So, in the current 11.0.0 previews we allow to filter on nested objects or arrays. It works like the following:
|
It is strange, because I get the following response: {
"errors": [
{
"message": "The specified input object field `documents` does not exist.",
"locations": [
{
"line": 2,
"column": 42
}
]
}
]
} But I noticed that autocomplete in playground offers me the following options: However none of them filter the
SELECT "m"."Id", "m"."CreatedAt", "m"."Name", "d"."Id", "d"."CreatedAt", "d"."MasterId", "d"."Name"
FROM "Masters" AS "m"
LEFT JOIN "Document" AS "d" ON "m"."Id" = "d"."MasterId"
WHERE ("m"."Name" LIKE 'M%') AND EXISTS (
SELECT 1
FROM "Document" AS "d0"
WHERE ("m"."Id" = "d0"."MasterId") AND ("d0"."Id" > 0))
ORDER BY "m"."Id", "d"."Id" But what I want is to get all {
masters {
name,
documents(where: {id_gt: 0}) {
createdAt,
id
}
}
} And as I said, using this query I get correct results, but the filter condition ( |
@marian-margeta beat me to the punch describing the issue. However, I am posting it because it goes into further detail. After looking more into this, I believe there is a misunderstanding about what was requested, and the solution provided. From what I understand, The request is to be able to construct a query such that the documents per individual master is filtered without further filtering the masters. Example Dataset:masters:
-
id: 37
name: L
documents:
- { id : 10 , name : initial document }
- { id : 51, name : extra document }
-
id: 52
name: M1
documents:
- { id : 0 , name : initial document (unsaved) }
- { id : 0, name : extra document (unsaved) }
-
id: 61
name: M2
documents:
- { id : 75 , name : initial document }
- { id : 0, name : extra document (unsaved) }
-
id: 25
name: M3
documents:
- { id : 19 , name : initial document }
- { id : 55, name : extra document }
-
id: 37
name: N
documents:
- { id : 94 , name : initial document }
- { id : 116, name : extra document } Expected resultsfiltered_masters:
-
id: 52
name: M1
documents: []
-
id: 61
name: M2
documents:
- { id : 75 , name : initial document }
-
id: 25
name: M3
documents:
- { id : 19 , name : initial document }
- { id : 55, name : extra document } As stated above, for a filter of a nested list type, rather than getting a What appears to be actually requested isn't so much as a nested filter as has been defined for HotChocolate, but, rather, an inner filter; one where the filtering of a nested property does not affect the filter of the top level object. For such an inner filter to work with the currently being built selection middleware, either the filter middleware would have to be able to send it's generated query to the selection middleware so that it would be able to create the proper query, or the selection middleware itself would have to handle filtering as well. In my projection middleware, I do this by checking to see if any where parameter was passed to a nested list property, make use of the https://github.com/TheJayMann/HotChocolate/blob/master/HotChocolate.Types.Projections/QueryableProjectionFilterVisitor.cs |
I think the example I gave in the first post should be clear enough: {
masters(order_by: {createdAt:DESC}, where: {name_starts_with: "M"}) {
name,
documents(order_by: {name: ASC}, where: {id_gt: 0}) {
createdAt,
id
}
}
} equivalent LINQ expression: context.Masters
.AsNoTracking()
.Where(x => x.Name.StartsWith("M"))
.OrderByDescending(x => x.CreatedAt)
.Select(x => new Master()
{
Name = x.Name,
Documents = x.Documents
.Where(z => z.Id > 0)
.OrderBy(z => z.Name)
.Select(z => new Document()
{
Id = z.Id,
CreatedAt = z.CreatedAt
})
.ToList()
}); This is how I can imagine Entity Framework Core support. |
@marian-margeta Were you able to get this working in the meantime? I'm trying to find a workaround until version 11 is published, but so far I have not found a solution |
@sgabler-solytic have you looked at rc.1 |
@PascalSenn this should work now, or? |
yes this should ne working with 10.4 and UseSelection |
https://github.com/ChilliCream/graphql-workshop/blob/master/src/Server/PureCodeFirst%2BEF /// <summary>
/// Gets access to all the people known to this service.
/// </summary>
// [Authorize]
[UsePaging]
[UseSelection]
[UseFiltering]
[UseSorting]
public IQueryable<Person> GetPeople(
[Service]ChatDbContext dbContext) =>
dbContext.People; |
@michaelstaib thanks, i'll check it out @PascalSenn thanks for the example |
@PascalSenn are you sure that it's already in 10.4.0-rc.1? I updated locally, but I neither have the Am I doing something wrong maybe? |
you need to add the package... HotChocolate.Types.Selections |
Thanks, it works great! 👍 Will filtering on related properties also be possible in 10.4, or will this come in version 11? Example: descriptor
.Filter(user => user.Contact.FirstName)
.AllowEquals().And()
.AllowContains().And()
.AllowStartsWith().And()
.AllowEndsWith().And()
.BindFiltersExplicitly(); The Contact field of a User can be nicely loaded via |
@sgabler-solytic this is already in place in v11 :) |
Hey I just tried it and its awesome works nice with ef core. userOne: user(userId:1) {
username,
email
},
userTwo: user(userId: 2) {
username,
email
} My only Idea to realize this would be to save all selected fields per entity and then run one big query ( in the batch loader ) which just runs select (getAllSelectedFieldsPerRequest...) from xyz where .... this wouldn't be a true |
Hi @S0PEX |
|
I tried this new selection feature just a few days ago and it works perfectly. Thank you for this! I have played with this a bit and now I am trying to figure out how to use it in my queries. I have some questions in terms of extensibility of the LINQ expression creation. I have 3 entities - class User
{
public long Id { get; set; }
public string Name { get; set; }
public byte[] Thumbnail { get; set; }
public List<UserDocRel> UserDocRels { get; set; }
}
class UserDocRel
{
public long UserId { get; set; }
public long DocumentId { get; set; }
public Document Document { get; set; }
public User User { get; set; }
}
class Document
{
public long Id { get; set; }
public string Text { get; set; }
public List<UserDocRel> UserDocRels { get; set; }
} Ideally, I would like to end with this graphql user type type User {
name: String
thumbnail: String # thumbnail encoded in base64
documents: [Document] # documents of this user
} I am struggling with DocumentsTo get all user documents, I need to include the following expression, before it is compiled into SQL Documents = UserDocRels.Select(x => x.Document).ToList(); Is there any approach to do this? It would be nice to have it outside of my entity object. For example in type configuration, instead of resolver: public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor
.Field("documents")
.SelectionExpression<User>(c => c.UserDocRels.Select(x => x.Document)); // of course, this does not exist
}
} ThumbnailsMaybe more challenging is the thumbnail field. In the entity object It is defined as a By the way, I noticed that it is not possible to select the tumbnail as an array. And in general, all enumerable properties of non-object types are not selectable. I get this error: "errors": [
{
"message": "UseSelection is in a invalid state. Type Byte is illegal!"
}
] The same for array of strings, ints, bools, etc. |
Both of your examples would likely be easier if you were to implement DTOs rather than exposing the raw entity types directly to GraphQL. This will allow you insert the What I am seeing for your implementation would be something like the following. Note that I am typing directly in the response without actually testing anything in code, so there may be easy to fix mistakes. public IQueryable<UserDTO> GetUsers([Service] UserDbContext db) =>
db.Users.Select(u => new UserDTO() {
Name = u.Name,
Thumbnail = Convert.ToBase64(u.Thumbnail),
Documents = u.UserDocRels.Select(ud => new DocumentDTO(){
Id = ud.Document.Id,
Text = ud.Document.Text,
}),
})
; I have done many test with this, and Entity Framework Core is still smart enough that if your query does not include the |
@TheJayMann Thank you for your quick response. I am familiar with DTO pattern, I use it frequently with automapper in all of my REST APIs but for some reason I didn't try it here :). I Tried it out and it works like a charm! Thanks again. Anyway, selection of properties whose type is an array of the primitive types doesn't seems to work. I get error bellow. I don't know if it is a bug or known limitation. "errors": [
{
"message": "UseSelection is in a invalid state. Type Byte is illegal!"
}
] |
So I'm not exactly sure why Hotchocolate is trying to do anything with a Byte at this point. If you've properly typed |
This is working awesome for me in version 11. I've been seeing some recommendations that version 11 is not ready for production but we need this for our project to go live. Would there be an easy way to back port this to version 10, or is it safe to be running version 11 in production? |
@TheJayMann It works fine If I convert thumbnail to base64 while projecting to DTO. The problem occurs if I want to get this thumbnail as a byte array for some reason (if I turn off byte64 conversion and change the type from string to byte[]). I don't want to project any arrays at this time but I think it could be supported anyway. @twilly86 I am not sure what is not implemented in version 10 but I am trying filtration and selection features over |
@TheJayMann Currently, I have a User Entity:
then I have my Products Entity:
My current issue is that when I am exposing the user through this dto pattern like: public IQueryable<UserDTO> GetUsers([Service] UserDbContext db) =>
db.Users.Select(u => new UserDTO() {
Id = u.Id
Name = u.Name,
OwnedProducts = u.OwnedByUsers.Select(ud => new ProductDTO(){
Id = ud.Product.Id,
Title = ud.Product.Title,
OwnedBy = ud.Product.OwnedByUsers.Select(ou => new UsersDto...
}); So basically I end up in infinite recursion because the User as multiple products and the products again have this owners and so on. Do you have a tip when dealing with such a situation? Regards Artur |
With this design, I believe a business decision should be made; how much should recursion be allowed? There must be a point at which attempting further recursion doesn't make sense, or possibly affects performance to attempt to get all the data at one time. If you truly do need to make potentially infinite recursion available, or at least allow a large level of recursion, you will likely either need to set up the select query in a recursive manner, or possibly write your own middleware which can take the input query and translate it into the proper dynamic linq query. If this is something that many people want, then perhaps a feature request (or even a pull request) could be made for HotChocolate. |
Regarding DTOs and projection, is there a recommended way to project my IQueryable to a DTO using AutoMapper? [UseSelection]
public IQueryable<TitleDto> GetTitles([Service] DbContext dbContext, [Service] IMapper mapper)
{
var titles = dbContext.Titles.ProjectTo<TitleDto>(mapper.ConfigurationProvider);
return titles;
} In my current example the projection to AutoMapper appears to "overwrite" the |
Support for DTOs (projections) is required - because we don't want to expose the mapping tables that are required for 1-N, N-N relationships (with additional data in the mapping table). The consumers of the API (GraphQL or REST) doesn't need to know these internals. |
[UseSelection] Not Working HotChocolate.AspNetCore 11.3.2 Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request. HotChocolate.SchemaException: For more details look at the
|
@Abdullah-onGit you can't mix versions of Hot Chocolate packages. If you want to use Projections with v11, you'll have to use the HotChocolate.Data package with the same version as your HotChocolate.AspNetCore package (11.3.2) |
Closing this issue, since all of the things the original issue talks about are supported in v11 to my knowledge. If there are other requests in this thread that are yet to be implemented/fixed, please create a separate issue for those and make sure they aren't yet addressed in the latest preview. Also please keep the issue focused on one specific thing and not multiple things, as these issues are harder to track. |
Thank you so much for your replay ❤ How about in DataLoaders? lets take example from 3-understanding-dataLoader.md
How to use projection here? |
@Abdullah-onGit I don't think you can use projections in DataLoaders. A DataLoader's job is to fetch an entity based on its Id. So for the same Id you should always get the same entity. The DataLoader therefore can't take an individual subselection of fields into account when fetching the entity, and thus you can not use Projections here. Projections only work if you are directly translating your GraphQL query into a database query. If you have custom resolvers/Data Loaders inbetween, the query can only be projected until it hits one of these unsupported fields. @PascalSenn please correct me, if what I'm saying is not accurate. |
Can someone provide github sample for "nested filters and sorts" with HotChocolate.Data.EntityFramework v11.3.0? Im Implementation in Query.cs file.
|
I am curious whether the Hot Chocolate has a support for Entity Framework Core. I can't find any information or examples. It seems that the type
IQueryable
is supported but the result with the use of the EF core is not as great as it could be.Filtering and sorting over the root entity seems to work fine but projecting doesn't. Always all columns are returned from the database, not only the requested ones. Nested entities (many-to-one relations) are completely ignored.
Let's say I have the two entities:
and the query:
I use the following GraphQL query:
the result I get doesn't contain documents as the join is missing in the SQL query:
I would expect the query to be parsed into this Linq query/expression:
which is translated into the correct SQL (SQLite provider 3.1.1):
Do you plan to support such functionality in Hot Chocolate? It would be nice to have it out of the box.
The text was updated successfully, but these errors were encountered: