|
| 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 | +``` |
0 commit comments