@@ -22,66 +22,76 @@ Processing a request involves the following steps:
2222 - `JsonApiResourceService` contains no more usage of `IQueryable`.
2323- ` EntityFrameworkCoreRepository ` delegates to ` QueryableBuilder ` to transform the ` QueryLayer ` tree into ` IQueryable ` expression trees.
2424 `QueryBuilder` depends on `QueryClauseBuilder` implementations that visit the tree nodes, transforming them to `System.Linq.Expression` equivalents.
25- The `IQueryable` expression trees are executed by Entity Framework Core, which produces SQL statements out of them.
25+ The `IQueryable` expression trees are passed to Entity Framework Core, which produces SQL statements out of them.
2626- ` JsonApiWriter ` transforms resource objects into json response.
2727
2828# Example
2929To get a sense of what this all looks like, let's look at an example query string:
3030
3131```
3232/api/v1/blogs?
33- include=owner,articles.revisions .author&
34- filter=has(articles )&
35- sort=count(articles )&
33+ include=owner,posts.comments .author&
34+ filter=has(posts )&
35+ sort=count(posts )&
3636 page[number]=3&
3737 fields[blogs]=title&
38- filter[articles ]=and(not(equals(author.firstName ,null)),has(revisions ))&
39- sort[articles ]=author.lastName &
40- fields[articles ]=url&
41- filter[articles.revisions ]=and(greaterThan(publishTime ,'2001-01-01 '),startsWith(author.firstName ,'J'))&
42- sort[articles.revisions ]=-publishTime ,author.lastName &
43- fields[revisions]=publishTime
38+ filter[posts ]=and(not(equals(author.userName ,null)),has(comments ))&
39+ sort[posts ]=author.displayName &
40+ fields[blogPosts ]=url&
41+ filter[posts.comments ]=and(greaterThan(createdAt ,'2001-01-01Z '),startsWith(author.userName ,'J'))&
42+ sort[posts.comments ]=-createdAt ,author.displayName &
43+ fields[comments]=createdAt
4444```
4545
4646After parsing, the set of scoped expressions is transformed into the following tree by ` QueryLayerComposer ` :
4747
4848```
4949QueryLayer<Blog>
5050{
51- Include: owner,articles.revisions
52- Filter: has(articles )
53- Sort: count(articles )
51+ Include: owner,posts.comments.author
52+ Filter: has(posts )
53+ Sort: count(posts )
5454 Pagination: Page number: 3, size: 5
55- Projection
55+ Selection
5656 {
57- title
58- id
59- owner: QueryLayer<Author>
57+ FieldSelectors<Blog>
6058 {
61- Sort: id
62- Pagination: Page number: 1, size: 5
63- }
64- articles: QueryLayer<Article>
65- {
66- Filter: and(not(equals(author.firstName,null)),has(revisions))
67- Sort: author.lastName
68- Pagination: Page number: 1, size: 5
69- Projection
59+ title
60+ id
61+ posts: QueryLayer<BlogPost>
7062 {
71- url
72- id
73- revisions: QueryLayer<Revision>
63+ Filter: and(not(equals(author.userName,null)),has(comments))
64+ Sort: author.displayName
65+ Pagination: Page number: 1, size: 5
66+ Selection
7467 {
75- Filter: and(greaterThan(publishTime,'2001-01-01'),startsWith(author.firstName,'J'))
76- Sort: -publishTime,author.lastName
77- Pagination: Page number: 1, size: 5
78- Projection
68+ FieldSelectors<BlogPost>
7969 {
80- publishTime
70+ url
8171 id
72+ comments: QueryLayer<Comment>
73+ {
74+ Filter: and(greaterThan(createdAt,'2001-01-01'),startsWith(author.userName,'J'))
75+ Sort: -createdAt,author.displayName
76+ Pagination: Page number: 1, size: 5
77+ Selection
78+ {
79+ FieldSelectors<Comment>
80+ {
81+ createdAt
82+ id
83+ author: QueryLayer<WebAccount>
84+ {
85+ }
86+ }
87+ }
88+ }
8289 }
8390 }
8491 }
92+ owner: QueryLayer<WebAccount>
93+ {
94+ }
8595 }
8696 }
8797}
@@ -90,36 +100,86 @@ QueryLayer<Blog>
90100Next, the repository translates this into a LINQ query that the following C# code would represent:
91101
92102``` c#
93- var query = dbContext .Blogs
103+ IQueryable < Blog > query = dbContext .Blogs
104+ .Include (" Posts.Comments.Author" )
94105 .Include (" Owner" )
95- .Include (" Articles.Revisions" )
96- .Where (blog => blog .Articles .Any ())
97- .OrderBy (blog => blog .Articles .Count )
106+ .Where (blog => blog .Posts .Any ())
107+ .OrderBy (blog => blog .Posts .Count )
98108 .Skip (10 )
99109 .Take (5 )
100110 .Select (blog => new Blog
101111 {
102112 Title = blog .Title ,
103113 Id = blog .Id ,
104- Owner = blog .Owner ,
105- Articles = new List <Article >(blog .Articles
106- .Where (article => article .Author .FirstName != null && article .Revisions .Any ())
107- .OrderBy (article => article .Author .LastName )
114+ Posts = blog .Posts
115+ .Where (blogPost => blogPost .Author .UserName != null && blogPost .Comments .Any ())
116+ .OrderBy (blogPost => blogPost .Author .DisplayName )
108117 .Take (5 )
109- .Select (article => new Article
118+ .Select (blogPost => new BlogPost
110119 {
111- Url = article .Url ,
112- Id = article .Id ,
113- Revisions = new HashSet <Revision >(article .Revisions
114- .Where (revision => revision .PublishTime > DateTime .Parse (" 2001-01-01" ) && revision .Author .FirstName .StartsWith (" J" ))
115- .OrderByDescending (revision => revision .PublishTime )
116- .ThenBy (revision => revision .Author .LastName )
120+ Url = blogPost .Url ,
121+ Id = blogPost .Id ,
122+ Comments = blogPost .Comments
123+ .Where (comment => comment .CreatedAt > DateTime .Parse (" 2001-01-01Z" ) &&
124+ comment .Author .UserName .StartsWith (" J" ))
125+ .OrderByDescending (comment => comment .CreatedAt )
126+ .ThenBy (comment => comment .Author .DisplayName )
117127 .Take (5 )
118- .Select (revision => new Revision
128+ .Select (comment => new Comment
119129 {
120- PublishTime = revision .PublishTime ,
121- Id = revision .Id
122- }))
123- }))
130+ CreatedAt = comment .CreatedAt ,
131+ Id = comment .Id ,
132+ Author = comment .Author
133+ }).ToHashSet ()
134+ }).ToList (),
135+ Owner = blog .Owner
124136 });
125137```
138+
139+ The LINQ query gets translated by Entity Framework Core into the following SQL:
140+
141+ ``` sql
142+ SELECT t." Title" , t." Id" , a." Id" , t2." Url" , t2." Id" , t2." Id0" , t2." CreatedAt" , t2." Id1" , t2." Id00" , t2." DateOfBirth" , t2." DisplayName" , t2." EmailAddress" , t2." Password" , t2." PersonId" , t2." PreferencesId" , t2." UserName" , a." DateOfBirth" , a." DisplayName" , a." EmailAddress" , a." Password" , a." PersonId" , a." PreferencesId" , a." UserName"
143+ FROM (
144+ SELECT b." Id" , b." OwnerId" , b." Title" , (
145+ SELECT COUNT (* )::INT
146+ FROM " Posts" AS p0
147+ WHERE b." Id" = p0." ParentId" ) AS c
148+ FROM " Blogs" AS b
149+ WHERE EXISTS (
150+ SELECT 1
151+ FROM " Posts" AS p
152+ WHERE b." Id" = p." ParentId" )
153+ ORDER BY (
154+ SELECT COUNT (* )::INT
155+ FROM " Posts" AS p0
156+ WHERE b." Id" = p0." ParentId" )
157+ LIMIT @__Create_Item1_1 OFFSET @__Create_Item1_0
158+ ) AS t
159+ LEFT JOIN " Accounts" AS a ON t." OwnerId" = a." Id"
160+ LEFT JOIN LATERAL (
161+ SELECT t0." Url" , t0." Id" , t0." Id0" , t1." CreatedAt" , t1." Id" AS " Id1" , t1." Id0" AS " Id00" , t1." DateOfBirth" , t1." DisplayName" , t1." EmailAddress" , t1." Password" , t1." PersonId" , t1." PreferencesId" , t1." UserName" , t0." DisplayName" AS " DisplayName0" , t1." ParentId"
162+ FROM (
163+ SELECT p1." Url" , p1." Id" , a0." Id" AS " Id0" , a0." DisplayName"
164+ FROM " Posts" AS p1
165+ LEFT JOIN " Accounts" AS a0 ON p1." AuthorId" = a0." Id"
166+ WHERE (t." Id" = p1." ParentId" ) AND (((a0." UserName" IS NOT NULL )) AND EXISTS (
167+ SELECT 1
168+ FROM " Comments" AS c
169+ WHERE p1." Id" = c." ParentId" ))
170+ ORDER BY a0." DisplayName"
171+ LIMIT @__Create_Item1_1
172+ ) AS t0
173+ LEFT JOIN (
174+ SELECT t3." CreatedAt" , t3." Id" , t3." Id0" , t3." DateOfBirth" , t3." DisplayName" , t3." EmailAddress" , t3." Password" , t3." PersonId" , t3." PreferencesId" , t3." UserName" , t3." ParentId"
175+ FROM (
176+ SELECT c0." CreatedAt" , c0." Id" , a1." Id" AS " Id0" , a1." DateOfBirth" , a1." DisplayName" , a1." EmailAddress" , a1." Password" , a1." PersonId" , a1." PreferencesId" , a1." UserName" , c0." ParentId" , ROW_NUMBER() OVER(PARTITION BY c0." ParentId" ORDER BY c0." CreatedAt" DESC , a1." DisplayName" ) AS row
177+ FROM " Comments" AS c0
178+ LEFT JOIN " Accounts" AS a1 ON c0." AuthorId" = a1." Id"
179+ WHERE (c0." CreatedAt" > @__Create_Item1_2) AND ((@__Create_Item1_3 = ' ' ) OR (((a1." UserName" IS NOT NULL )) AND ((a1." UserName" LIKE @__Create_Item1_3 || ' %' ESCAPE ' ' ) AND (left(a1." UserName" , length(@__Create_Item1_3))::text = @__Create_Item1_3::text ))))
180+ ) AS t3
181+ WHERE t3 .row <= @__Create_Item1_1
182+ ) AS t1 ON t0." Id" = t1." ParentId"
183+ ) AS t2 ON TRUE
184+ ORDER BY t .c , t." Id" , a." Id" , t2." DisplayName0" , t2." Id" , t2." Id0" , t2." ParentId" , t2." CreatedAt" DESC , t2." DisplayName" , t2." Id1"
185+ ```
0 commit comments