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:
JsonApiMiddlewarecollects resource info from routing data for the current request.JsonApiReadertransforms json request body into objects.JsonApiControlleraccepts get/post/patch/delete verb and delegates to service.IQueryStringParameterReaders delegate toQueryParsers that transform query string text intoQueryExpressionobjects.- 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 throughExpressionInScopeobjects.
QueryLayerComposer(used fromJsonApiResourceService) collects all query constraints.- It combines them with default options and
IResourceDefinitionoverrides and composes a tree ofQueryLayerobjects. - It lifts the tree for secondary endpoints like /blogs/1/articles and rewrites includes.
JsonApiResourceServicecontains no more usage ofIQueryable.
- It combines them with default options and
EntityFrameworkCoreRepositorydelegates toQueryableBuilderto transform theQueryLayertree intoIQueryableexpression trees.QueryBuilderdepends onQueryClauseBuilderimplementations that visit the tree nodes, transforming them toSystem.Linq.Expressionequivalents. TheIQueryableexpression trees are passed to Entity Framework Core, which produces SQL statements out of them.JsonApiWritertransforms 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"