Processing queries
since v4.0
The query pipeline roughly looks like this:
flowchart TB
A[HTTP] -->|ASP.NET| B(QueryString) -->|JADNC:QueryStringParameterReader| C("QueryExpression[]") -->|JADNC:ResourceService| D(QueryLayer) -->|JADNC:Repository| E(IQueryable) -->|Entity Framework Core| F[(SQL)]
Processing a request involves the following steps:
JsonApiMiddleware
collects resource info from routing data for the current request.JsonApiReader
transforms json request body into objects.JsonApiController
accepts get/post/patch/delete verb and delegates to service.IQueryStringParameterReader
s delegate toQueryParser
s that transform query string text intoQueryExpression
objects.- By using prefix notation in filters, we don't need users to remember operator precedence and associativity rules.
- These validated expressions contain direct references to attributes and relationships.
- The readers also implement
IQueryConstraintProvider
, which exposes expressions throughExpressionInScope
objects.
QueryLayerComposer
(used fromJsonApiResourceService
) collects all query constraints.- It combines them with default options and
IResourceDefinition
overrides and composes a tree ofQueryLayer
objects. - It lifts the tree for secondary endpoints like /blogs/1/articles and rewrites includes.
JsonApiResourceService
contains no more usage ofIQueryable
.
- It combines them with default options and
EntityFrameworkCoreRepository
delegates toQueryableBuilder
to transform theQueryLayer
tree intoIQueryable
expression trees.QueryBuilder
depends onQueryClauseBuilder
implementations that visit the tree nodes, transforming them toSystem.Linq.Expression
equivalents. TheIQueryable
expression trees are passed to Entity Framework Core, which produces SQL statements out of them.JsonApiWriter
transforms resource objects into json response.
Example
To get a sense of what this all looks like, let's look at an example query string:
/api/blogs?
include=owner,posts.comments.author&
filter=has(posts)&
sort=count(posts)&
page[number]=3&
fields[blogs]=title&
filter[posts]=and(not(equals(author.userName,null)),has(comments))&
sort[posts]=author.displayName&
fields[blogPosts]=url&
filter[posts.comments]=and(greaterThan(createdAt,'2001-01-01Z'),startsWith(author.userName,'J'))&
sort[posts.comments]=-createdAt,author.displayName&
fields[comments]=createdAt
After parsing, the set of scoped expressions is transformed into the following tree by QueryLayerComposer
:
QueryLayer<Blog>
{
Include: owner,posts.comments.author
Filter: has(posts)
Sort: count(posts)
Pagination: Page number: 3, size: 5
Selection
{
FieldSelectors<Blog>
{
title
id
posts: QueryLayer<BlogPost>
{
Filter: and(not(equals(author.userName,null)),has(comments))
Sort: author.displayName
Pagination: Page number: 1, size: 5
Selection
{
FieldSelectors<BlogPost>
{
url
id
comments: QueryLayer<Comment>
{
Filter: and(greaterThan(createdAt,'2001-01-01'),startsWith(author.userName,'J'))
Sort: -createdAt,author.displayName
Pagination: Page number: 1, size: 5
Selection
{
FieldSelectors<Comment>
{
createdAt
id
author: QueryLayer<WebAccount>
{
}
}
}
}
}
}
}
owner: QueryLayer<WebAccount>
{
}
}
}
}
Next, the repository translates this into a LINQ query that the following C# code would represent:
IQueryable<Blog> query = dbContext.Blogs
.Include("Posts.Comments.Author")
.Include("Owner")
.Where(blog => blog.Posts.Any())
.OrderBy(blog => blog.Posts.Count)
.Skip(10)
.Take(5)
.Select(blog => new Blog
{
Title = blog.Title,
Id = blog.Id,
Posts = blog.Posts
.Where(blogPost => blogPost.Author.UserName != null && blogPost.Comments.Any())
.OrderBy(blogPost => blogPost.Author.DisplayName)
.Take(5)
.Select(blogPost => new BlogPost
{
Url = blogPost.Url,
Id = blogPost.Id,
Comments = blogPost.Comments
.Where(comment => comment.CreatedAt > DateTime.Parse("2001-01-01Z") &&
comment.Author.UserName.StartsWith("J"))
.OrderByDescending(comment => comment.CreatedAt)
.ThenBy(comment => comment.Author.DisplayName)
.Take(5)
.Select(comment => new Comment
{
CreatedAt = comment.CreatedAt,
Id = comment.Id,
Author = comment.Author
}).ToHashSet()
}).ToList(),
Owner = blog.Owner
});
The LINQ query gets translated by Entity Framework Core into the following SQL:
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"
FROM (
SELECT b."Id", b."OwnerId", b."Title", (
SELECT COUNT(*)::INT
FROM "Posts" AS p0
WHERE b."Id" = p0."ParentId") AS c
FROM "Blogs" AS b
WHERE EXISTS (
SELECT 1
FROM "Posts" AS p
WHERE b."Id" = p."ParentId")
ORDER BY (
SELECT COUNT(*)::INT
FROM "Posts" AS p0
WHERE b."Id" = p0."ParentId")
LIMIT @__Create_Item1_1 OFFSET @__Create_Item1_0
) AS t
LEFT JOIN "Accounts" AS a ON t."OwnerId" = a."Id"
LEFT JOIN LATERAL (
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"
FROM (
SELECT p1."Url", p1."Id", a0."Id" AS "Id0", a0."DisplayName"
FROM "Posts" AS p1
LEFT JOIN "Accounts" AS a0 ON p1."AuthorId" = a0."Id"
WHERE (t."Id" = p1."ParentId") AND (((a0."UserName" IS NOT NULL)) AND EXISTS (
SELECT 1
FROM "Comments" AS c
WHERE p1."Id" = c."ParentId"))
ORDER BY a0."DisplayName"
LIMIT @__Create_Item1_1
) AS t0
LEFT JOIN (
SELECT t3."CreatedAt", t3."Id", t3."Id0", t3."DateOfBirth", t3."DisplayName", t3."EmailAddress", t3."Password", t3."PersonId", t3."PreferencesId", t3."UserName", t3."ParentId"
FROM (
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
FROM "Comments" AS c0
LEFT JOIN "Accounts" AS a1 ON c0."AuthorId" = a1."Id"
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))))
) AS t3
WHERE t3.row <= @__Create_Item1_1
) AS t1 ON t0."Id" = t1."ParentId"
) AS t2 ON TRUE
ORDER BY t.c, t."Id", a."Id", t2."DisplayName0", t2."Id", t2."Id0", t2."ParentId", t2."CreatedAt" DESC, t2."DisplayName", t2."Id1"