Skip to content

Commit bcbf04a

Browse files
author
Bart Koelman
committed
Added overview documentation
1 parent 00fb847 commit bcbf04a

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

docs/docfx.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"getting-started/**/toc.yml",
2626
"usage/**.md",
2727
"request-examples/**.md",
28+
"internals/**.md",
2829
"toc.yml",
2930
"*.md"
3031
]

docs/internals/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Internals
2+
3+
The section contains overviews for the inner workings of the JsonApiDotNetCore library.

docs/internals/queries.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Processing queries
2+
3+
_since v4.0_
4+
5+
The query pipeline roughly looks like this:
6+
7+
```
8+
HTTP --[ASP.NET Core]--> QueryString --[JADNC:QueryStringParameterReader]--> QueryExpression[] --[JADNC:ResourceService]--> QueryLayer --[JADNC:Repository]--> IQueryable --[EF Core]--> SQL
9+
```
10+
11+
Processing a request involves the following steps:
12+
- `JsonApiMiddleware` collects resource info from routing data for the current request.
13+
- `JsonApiReader` transforms json request body into objects.
14+
- `JsonApiController` accepts get/post/patch/delete verb and delegates to service.
15+
- `IQueryStringParameterReader`s delegate to `QueryParser`s that transform query string text into `QueryExpression` objects.
16+
- By using prefix notation in filters, we don't need users to remember operator precedence and associativity rules.
17+
- These validated expressions contain direct references to attributes and relationships.
18+
- The readers also implement `IQueryConstraintProvider`, which exposes expressions through `ExpressionInScope` objects.
19+
- `QueryLayerComposer` (used from `JsonApiResourceService`) collects all query constraints.
20+
- It combines them with default options and `ResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
21+
- It lifts the tree for nested endpoints like /blogs/1/articles and rewrites includes.
22+
- `JsonApiResourceService` contains no more usage of `IQueryable`.
23+
- `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees.
24+
`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 EF Core, which produces SQL statements out of them.
26+
- `JsonApiWriter` transforms resource objects into json response.
27+
28+
# Example
29+
To get a sense of what this all looks like, let's look at an example query string:
30+
31+
```
32+
/api/v1/blogs?
33+
include=owner,articles.revisions.author&
34+
filter=has(articles)&
35+
sort=count(articles)&
36+
page[number]=3&
37+
fields=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[articles.revisions]=publishTime
44+
```
45+
46+
After parsing, the set of scoped expressions is transformed into the following tree by `QueryLayerComposer`:
47+
48+
```
49+
QueryLayer<Blog>
50+
{
51+
Include: owner,articles.revisions
52+
Filter: has(articles)
53+
Sort: count(articles)
54+
Pagination: Page number: 3, size: 5
55+
Projection
56+
{
57+
title
58+
id
59+
owner: QueryLayer<Author>
60+
{
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
70+
{
71+
url
72+
id
73+
revisions: QueryLayer<Revision>
74+
{
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
79+
{
80+
publishTime
81+
id
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
```
89+
90+
Next, the repository translates this into a LINQ query that the following C# code would represent:
91+
92+
```c#
93+
var query = dbContext.Blogs
94+
.Include("Owner")
95+
.Include("Articles.Revisions")
96+
.Where(blog => blog.Articles.Any())
97+
.OrderBy(blog => blog.Articles.Count)
98+
.Skip(10)
99+
.Take(5)
100+
.Select(blog => new Blog
101+
{
102+
Title = blog.Title,
103+
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)
108+
.Take(5)
109+
.Select(article => new Article
110+
{
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)
117+
.Take(5)
118+
.Select(revision => new Revision
119+
{
120+
PublishTime = revision.PublishTime,
121+
Id = revision.Id
122+
}))
123+
}))
124+
});
125+
```

docs/internals/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# [Queries](queries.md)

docs/toc.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@
1414
#
1515
# - name: Request Examples
1616
# href: request-examples/
17-
# homepage: request-examples/index.md
17+
# homepage: request-examples/index.md
18+
19+
- name: Internals
20+
href: internals/
21+
homepage: internals/index.md

0 commit comments

Comments
 (0)