diff --git a/docs/client-api/csharp-code-analyzers/RVN001.mdx b/docs/client-api/csharp-code-analyzers/RVN001.mdx new file mode 100644 index 0000000000..f97f4f2eeb --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN001.mdx @@ -0,0 +1,167 @@ +--- +title: "RVN001 - Index Map or Reduce assigned outside constructor" +sidebar_label: RVN001 +sidebar_position: 1 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Creating and deploying indexes" + link: "indexes/creating-and-deploying" + source: "docs" + path: "Indexes" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" + - title: "Map-reduce indexes" + link: "indexes/map-reduce-indexes" + source: "docs" + path: "Indexes" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN001** reports assignments to an index's `Map` or `Reduce` property inside a method instead of directly in the constructor. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN001 reported?](#when-is-rvn001-reported) + * [When is RVN001 not reported?](#when-is-rvn001-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN001)](#before-triggers-rvn001) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +The RavenDB client builds the index definition from the `Map` and `Reduce` expressions when the index is deployed. +If these expressions are assigned in a separate method, that method may not run before the definition is built, +for example if it is called conditionally or not at all. + +For single-map document indexes, the client throws `InvalidOperationException` if `Map` is still unset when the definition is built. +If `Reduce` is unset, the resulting definition is map-only. + + + + + +RVN001 checks index classes derived from the following bases: + +- `AbstractIndexCreationTask`, `AbstractIndexCreationTask`, + or `AbstractGenericIndexCreationTask`. +- `AbstractMultiMapIndexCreationTask`, `AbstractMultiMapTimeSeriesIndexCreationTask`, + or `AbstractMultiMapCountersIndexCreationTask`, including their generic variants. + +Abstract index classes are checked too. +Multi-map indexes have no `Map` property, so only `Reduce` assignments apply to them. + +RVN001 also reports `Reduce` assignments in JavaScript indexes derived from `AbstractJavaScriptIndexCreationTask`. +Assignments to their `Maps` property are not checked. + + + +### When is RVN001 reported? + +RVN001 is reported when `Map` or `Reduce` is assigned inside an ordinary method of a supported index class, +including assignments inside lambdas or local functions nested in that method. +Both block-bodied and expression-bodied methods are checked. + +The assignment must use an unqualified property name (`Map` or `Reduce`), +`this.Map` / `this.Reduce`, or `base.Map` / `base.Reduce`. + +RVN001 does not trace calls. It is still reported when the method is always called from a constructor, +or when it reassigns a property the constructor already set. + + + + +### When is RVN001 not reported? + +RVN001 is not reported for: + +- Assignments in constructors, including lambdas or local functions nested in them. +- Assignments in property or indexer accessors, operators, or finalizers. +- Assignments to local variables or parameters named `Map` or `Reduce`. +- Assignments through another object, such as `other.Map` or `other.Reduce`. +- Calls to `AddMap` or `AddMapForAll` inside methods. +- Single-map time-series and counters indexes based on `AbstractTimeSeriesIndexCreationTask<...>` + or `AbstractCountersIndexCreationTask<...>`. + + + + + + + +Move the reported `Map` or `Reduce` assignment directly into the constructor body. + +--- + +### Before (triggers RVN001) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public void Configure() + { + // Triggers RVN001 even though the constructor always calls Configure(). + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } + + public Orders_ByCompany() + { + Configure(); + } +} +``` +
+ +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } +} +``` + +
+ + + +- A partial class declaration that lists neither a base class nor any interfaces is skipped. + Assignments in methods declared in that part are not checked, even when another part declares the index base class. + +- RVN001 can also report assignments to custom fields or properties named `Map` or `Reduce` on an index class, + even when they are unrelated to the index definition. + + + + + +* [RVN004](./RVN004.mdx) + Fires when no constructor sets `Map`, directly or through a method it calls. + RVN001 and RVN004 can both be reported when `Map` is assigned only in a method that no constructor calls. + +* [RVN005](./RVN005.mdx) + Fires when no constructor registers a map through `AddMap` or `AddMapForAll`, directly or through a method it calls. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN002.mdx b/docs/client-api/csharp-code-analyzers/RVN002.mdx new file mode 100644 index 0000000000..e9f3f2e640 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN002.mdx @@ -0,0 +1,209 @@ +--- +title: "RVN002 - RavenDB query operator after projection" +sidebar_label: RVN002 +sidebar_position: 2 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Project query results" + link: "client-api/session/querying/how-to-project-query-results" + source: "docs" + path: "Client API > Session > Querying" + - title: "Project index query results" + link: "indexes/querying/projections" + source: "docs" + path: "Indexes > Querying" + - title: "Filter query results" + link: "querying/filtering-query-results/filter-query-results" + source: "docs" + path: "Querying > Filtering Query Results" + - title: "Sort query results" + link: "querying/sorting-query-results/sort-query-results" + source: "docs" + path: "Querying > Sorting Query Results" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN002** detects query operators placed after a `ProjectInto` or `Select` projection where they can refer to the wrong fields. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Operators flagged by RVN002](#operators-flagged-by-rvn002) + * [After `ProjectInto`](#after-projectinto) + * [After `Select`](#after-select) + * [Operators not flagged](#operators-not-flagged) + * [Trigger condition](#trigger-condition) + * [When is RVN002 reported?](#when-is-rvn002-reported) + * [When is RVN002 not reported?](#when-is-rvn002-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN002)](#before-triggers-rvn002) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +RavenDB LINQ query operators such as `Where`, `OrderBy`, `Search`, `Spatial`, `VectorSearch`, `Filter`, `GroupBy`, +and their variants bind against the **source document or index fields**. + +A projection such as `.ProjectInto()` or `.Select(x => new { … })` can change the query's element type. +A lambda passed to an operator after the projection refers to members of the projected type, while the server still uses the source fields. +If a projected member's name does not match its source field's name, the query can silently produce incorrect results or throw at runtime. + +RVN002 reports operator and projection combinations that can cause these problems. +A reported query may still work when the referenced field names match; see [Limitations](#limitations). + + + + + +RVN002 checks the operators listed below after `ProjectInto` or `Select`. +After `Select`, some of these operators are not flagged; see [After `Select`](#after-select). + + + +### After `ProjectInto` + +After `ProjectInto`, the following operators are flagged: + - **Filtering:** `Where`, `Filter`. + - **Ordering:** `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`. + - **Ordering by score:** `OrderByScore`, `OrderByScoreDescending`, `ThenByScore`, `ThenByScoreDescending`. + - **Search:** `Search`, `MoreLikeThis`, `VectorSearch`. + - **Spatial:** `Spatial`, `OrderByDistance`, `OrderByDistanceDescending`. + - **Grouping:** `GroupBy`, `GroupByArrayValues`, `GroupByArrayContent`. + + + + +### After `Select` + +After `Select`, the operators listed under [After `ProjectInto`](#after-projectinto) are flagged, with the exceptions below. + +`Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, and `ThenByDescending` are not flagged when the `Select` either: + - Returns the source element unchanged, such as `o => o`. + - Creates an anonymous object in which **every member** directly copies a top-level member of the lambda parameter under the same name, + such as `o => new { o.Company, o.Total }` or `o => new { Company = o.Company }`. + +They are flagged after other projection forms, including renamed or computed members, nested member paths, DTO projections, +and members read from captured variables, such as `o => new { captured.Name }`. + + + + +### Operators not flagged + +Operators not in the list above, such as `Include`, `Skip`, and `Take`, are not flagged after a projection. + + + + + + + + + +### When is RVN002 reported? + +RVN002 checks explicit method calls in RavenDB LINQ queries (`session.Query()`, sync or async). +RVN002 is reported on the name of each flagged operator that follows `ProjectInto` or `Select`, +according to the conditions described in [Operators flagged by RVN002](#operators-flagged-by-rvn002). + +For each operator, RVN002 uses the nearest preceding `ProjectInto` or `Select` in the chain to determine which conditions apply. + +RVN002 follows projected queries through local-variable initializers, +so a flagged operator can appear in a later statement that uses the local variable. + + + + +### When is RVN002 not reported? + +- Operators placed before a projection, or not listed under [Operators flagged by RVN002](#operators-flagged-by-rvn002). +- `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, and `ThenByDescending` after a `Select` that meets the exceptions described in [After `Select`](#after-select). +- Operators and projections written as C# query-syntax clauses, such as `select … into … where …`. +- LINQ over in-memory collections, for example after `ToList()`, or other queries that do not use RavenDB's LINQ provider. +- Operators after `SelectMany` or `GroupBy`, when the chain contains no earlier `Select` or `ProjectInto`. + RVN002 does not treat `SelectMany` or `GroupBy` as projections. + + + + + + + +Apply filtering, ordering, and other source-based operators **before** the projection, using the original document or index fields. +Place the projection after these operators and before materializing the results, for example via `ToList()` or `ToListAsync()`. + +For `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, and `ThenByDescending` after `Select`, another option is to use an anonymous-object projection +in which every member directly copies a top-level member of the lambda parameter under the same name; see [After `Select`](#after-select). + +--- + +### Before (triggers RVN002) + +Here, `CompanyId` is a projection alias, not a source field. +The `Where` must use the source field `Company` before the projection. + +```csharp +var results = session.Query() + .Select(o => new { CompanyId = o.Company }) + // Triggers RVN002: Where uses the projected alias CompanyId. + .Where(x => x.CompanyId == "companies/1-A") + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +var results = session.Query() + .Where(o => o.Company == "companies/1-A") + .Select(o => new { CompanyId = o.Company }) + .Take(50) + .ToList(); +``` + +
+ + + +- After `ProjectInto`, RVN002 reports every listed operator without checking whether the referenced field exists under the same name in the source. + A reported query may therefore still work correctly. + +- After `Select`, the exception for `Where` and the four ordering operators depends on the **entire projection**, rather than only the members used by the operator. + A renamed or computed member causes these operators to be flagged even when they use another member that keeps its source name. For example: + + ```csharp + .Select(o => new { o.Company, RenamedTotal = o.Total }) + .Where(x => x.Company == "companies/1-A") + ``` + +- Tracking across statements is limited to local-variable initializers. Later assignments are not followed, + so diagnostics may be missed or based on an earlier query value. + +- RVN002 does not trace a projected query through helper-method returns, parameters, fields, or properties. + + + + + +* [RVN003](./RVN003.mdx) + Reports repeated `ProjectInto` calls in the same query chain. + +* [RVN007](./RVN007.mdx) + Reports fields used for filtering, sorting, or searching in a static-index query when the index's map function does not define them. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN003.mdx b/docs/client-api/csharp-code-analyzers/RVN003.mdx new file mode 100644 index 0000000000..e6492afba1 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN003.mdx @@ -0,0 +1,144 @@ +--- +title: "RVN003 - ProjectInto called more than once in a query chain" +sidebar_label: RVN003 +sidebar_position: 3 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Single projection per query" + link: "client-api/session/querying/how-to-project-query-results#single-projection-per-query" + source: "docs" + path: "Client API > Session > Querying" + - title: "Project index query results" + link: "indexes/querying/projections" + source: "docs" + path: "Indexes > Querying" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN003** reports repeated `ProjectInto` calls in the same RavenDB LINQ query chain. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN003 reported?](#when-is-rvn003-reported) + * [When is RVN003 not reported?](#when-is-rvn003-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN003)](#before-triggers-rvn003) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +RavenDB allows only a [single projection per query](../session/querying/how-to-project-query-results.mdx#single-projection-per-query), +whether the projection is defined with `ProjectInto()` or `Select(...)`. + +RVN003 detects repeated `ProjectInto` calls in the same LINQ query chain at compile time. +Calling `ProjectInto` a second time throws `InvalidOperationException` ("_'ProjectInto' was already called…_") +as soon as the second call is made, because the projection is already registered on the query provider. + +RVN003 does not report other combinations of projection methods, such as `ProjectInto` with `Select`. + + + + + +RVN003 checks RavenDB LINQ queries from synchronous or asynchronous sessions. + + + +### When is RVN003 reported? + +RVN003 is reported on each `ProjectInto` call that follows an earlier `ProjectInto` call in the same query chain. +This includes a projected query stored in a local variable and projected again in a later statement. + +The calls can target the same type or different types, and other query operators can appear between them. + + + + +### When is RVN003 not reported? + +RVN003 is not reported for: + +- A query with a single projection. +- A combination of `ProjectInto` and `Select`, or multiple `Select` calls. +- Separate `ProjectInto` calls that each start from the same local variable initialized with an unprojected query, + rather than from the result of an earlier projection. + +Separate branches are not necessarily independent query instances, so the absence of RVN003 does not guarantee +that reusing a LINQ query is safe. For the supported base-query reuse pattern, see [How to resolve](#how-to-resolve). + + + + + + + +Remove the duplicate `.ProjectInto` call. +If you need additional fields, use a single projection type that contains the fields you need. + +To run the same base query with multiple projections, keep the unprojected base query as a `DocumentQuery` +(or `AsyncDocumentQuery` for an asynchronous session). +Call `ToQueryable()` separately before each projection to create independent query instances. +See [Reusing the same base query for multiple projections](../session/querying/how-to-project-query-results.mdx#reusing-the-same-base-query-for-multiple-projections) for synchronous and asynchronous examples. + +--- + +### Before (triggers RVN003) + +```csharp +var projected = session.Query() + .ProjectInto(); + +var results = projected + // Triggers RVN003: the local variable already holds a projected query. + .ProjectInto() + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +var results = session.Query() + .ProjectInto() + .Take(50) + .ToList(); +``` + +
+ + + +- RVN003 follows only the value a local variable is declared with; later assignments are not taken into account. + Depending on the reassignment, this can miss a repeated projection or report one that is no longer present. + +- RVN003 does not trace earlier projections through helper-method return values, parameters, fields, properties, + or C# query-syntax clauses. Explicit repeated calls in a visible chain can still be detected. + +- Within a recognized RavenDB query chain, RVN003 identifies `ProjectInto` calls by name without verifying + that they resolve to RavenDB's extension method. Custom methods with the same name can also cause a report. + + + + + +* [RVN002](./RVN002.mdx) + Reports certain query operators used after a projection, including a `Where` placed between two `ProjectInto` calls. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN004.mdx b/docs/client-api/csharp-code-analyzers/RVN004.mdx new file mode 100644 index 0000000000..bfbe266785 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN004.mdx @@ -0,0 +1,161 @@ +--- +title: "RVN004 - AbstractIndexCreationTask subclass is missing a Map assignment" +sidebar_label: RVN004 +sidebar_position: 4 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Creating and deploying indexes" + link: "indexes/creating-and-deploying" + source: "docs" + path: "Indexes" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" + - title: "Map-reduce indexes" + link: "indexes/map-reduce-indexes" + source: "docs" + path: "Indexes" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN004** reports a missing `Map` assignment in a RavenDB index class. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN004 reported?](#when-is-rvn004-reported) + * [When is RVN004 not reported?](#when-is-rvn004-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN004)](#before-triggers-rvn004) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +Indexes that derive from `AbstractIndexCreationTask` or `AbstractIndexCreationTask` +and use the standard `CreateIndexDefinition()` implementation must assign `Map` before the client builds the index definition. + +Without a `Map`, the client throws `InvalidOperationException` ("_Map is required to generate an index…_") when it builds the index definition, +so the index is never deployed. + +When indexes are deployed together through `IndexCreation.CreateIndexes()` or `store.ExecuteIndexes()`, +a missing `Map` prevents every index in that batch from being deployed. + + + + + + + +### When is RVN004 reported? + +RVN004 is reported when no `Map` assignment is reachable from any constructor of the index class or its base classes, +including a base class from a referenced project. + +RVN004 is still reported when `Map` is assigned only in a method that no constructor calls, +even if that method runs before the client builds the index definition. + + + + +### When is RVN004 not reported? + +An assignment found in any constructor of the index class or its base classes is enough to suppress RVN004. +Assignments in methods of those classes also count when a constructor calls the method, directly or through other methods. + +RVN004 recognizes assignments written as `Map = ...`, `this.Map = ...`, or `base.Map = ...`. +See [Limitations](#limitations) for cases where an assignment counts even though it may not execute. + +RVN004 is also not reported for the following index classes, even when no `Map` assignment is found: + + - Abstract index classes. Their subclasses are checked instead. + - Indexes that override `CreateIndexDefinition()`. + - Multi-map indexes. See [RVN005](./RVN005.mdx). + - JavaScript indexes (`AbstractJavaScriptIndexCreationTask`), which define their maps as strings. + - Index classes that are `private`, `protected`, or `private protected`, or nested in a class with one of these access levels. + - Indexes whose required base-class information cannot be obtained from generated metadata or available source. + This can happen when a base class is in a compiled assembly built with a client version that does not include the RavenDB analyzers. + See [Distribution](./overview.mdx#distribution). + + + + + + + +Add a constructor (or update the existing one) that assigns the `Map` property. + +If the class is only a base for other indexes, declare it `abstract`. + +--- + +### Before (triggers RVN004) + +```csharp +// Triggers RVN004: Map is never assigned. +public class Orders_ByCompany : AbstractIndexCreationTask +{ +} +``` +
+ +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } +} +``` + +
+ + + +RVN004 does not verify that every constructor or execution path assigns `Map`. + +**RVN004 can be suppressed even when `Map` remains unset:** + +- An assignment in any constructor counts, including a base constructor that the index never calls. +- An assignment inside a condition counts even when the condition prevents it from executing. +- An assignment inside a lambda or local function nested in a checked constructor or method counts even if the lambda or local function is never invoked. +- Calls to methods are matched by name only. An assignment in another overload with the same name can therefore suppress RVN004 even when that overload is never called. + +**RVN004 can be reported even when `Map` is assigned at runtime:** + +- An assignment made only in a property setter is not detected, even if the constructor sets that property. +- An assignment made through another reference, such as `index.Map = ...`, is not recognized. +- A helper method defined in a base class from a referenced project is not followed when only a derived constructor calls it. + `Map` assignments reachable from that base class's own constructors still count through its generated index metadata. + + + + + +* [RVN001](./RVN001.mdx) + Fires when `Map` is assigned in a method rather than a constructor. + RVN001 and RVN004 can both be reported when `Map` is assigned only in a method that no constructor calls. + +* [RVN005](./RVN005.mdx) + Equivalent rule for multi-map indexes. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN005.mdx b/docs/client-api/csharp-code-analyzers/RVN005.mdx new file mode 100644 index 0000000000..dbe5ca1e7c --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN005.mdx @@ -0,0 +1,169 @@ +--- +title: "RVN005 - Multi-map index has no AddMap call in any constructor" +sidebar_label: RVN005 +sidebar_position: 5 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Multi-map indexes" + link: "indexes/multi-map-indexes" + source: "docs" + path: "Indexes" + - title: "Creating and deploying indexes" + link: "indexes/creating-and-deploying" + source: "docs" + path: "Indexes" + - title: "Indexing time series" + link: "document-extensions/timeseries/indexing" + source: "docs" + path: "Document Extensions > Time Series" + - title: "Indexing counters" + link: "document-extensions/counters/indexing" + source: "docs" + path: "Document Extensions > Counters" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN005** reports multi-map index classes that have no `AddMap` or `AddMapForAll` calls reachable from a constructor. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN005 reported?](#when-is-rvn005-reported) + * [When is RVN005 not reported?](#when-is-rvn005-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN005)](#before-triggers-rvn005) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +Every non-abstract class that derives from `AbstractMultiMapIndexCreationTask` +(or the time-series and counters variants: `AbstractMultiMapTimeSeriesIndexCreationTask`, `AbstractMultiMapCountersIndexCreationTask`) +and uses the standard `CreateIndexDefinition()` implementation must register at least one map with `AddMap` or `AddMapForAll` before the index definition is built. + +**Deployment failure**: +Without a map, the client throws `ArgumentNullException` while preparing the deployment request, before sending it to the server. +The exception identifies the parameter as `Index definitions contains no Maps` and does not name the index. + +**Batch deployment**: +When indexes are deployed together through `IndexCreation.CreateIndexes()` or `store.ExecuteIndexes()`, +an index with no maps prevents every index in that batch from being deployed. + + + + + + + +### When is RVN005 reported? + +* RVN005 is reported on the class name when no `AddMap` or `AddMapForAll` call is reachable from any constructor of the index class or its base classes, + including a base class from a referenced project. + +* RVN005 is still reported when maps are registered only in a method that is not reachable from a constructor, + even if that method runs before the index definition is built. + + + + +### When is RVN005 not reported? + +A call found in any constructor of the index class or its base classes is enough to suppress RVN005. +Calls in methods of those classes also count when a constructor calls the method, directly or through other methods. +See [Limitations](#limitations) for cases where a call counts even though it may not execute. + +RVN005 is also not reported for the following index classes, even when no `AddMap` or `AddMapForAll` call is found: + + - Abstract index classes. Their subclasses are checked instead. + - Indexes that override `CreateIndexDefinition()`. + - Single-map time-series and counters indexes (`AbstractTimeSeriesIndexCreationTask` and `AbstractCountersIndexCreationTask`). + Neither RVN004 nor RVN005 checks them. + - Index classes that are `private`, `protected`, or `private protected`, or nested in a class with one of these access levels. + - Indexes whose required base-class information cannot be obtained from generated metadata or available source. + This can happen when a base class is in a compiled assembly built with a client version that does not include the RavenDB analyzers. + See [Distribution](./overview.mdx#distribution). + + + + + + + +Add a constructor (or update the existing one) that calls `AddMap(…)` for each document type the index should map. +Alternatively, use `AddMapForAll(…)` to register maps for concrete types assignable to `TBase` in the assembly that defines `TBase`. + +For time-series and counters multi-map indexes, use `AddMap("", …)` or `AddMapForAll("", …)`, +passing the time-series or counter name. + +--- + +### Before (triggers RVN005) + +```csharp +// Triggers RVN005: no AddMap or AddMapForAll call. +public class Animals_ByName : AbstractMultiMapIndexCreationTask +{ +} +``` +
+ +### After (correct) + +```csharp +public class Animals_ByName : AbstractMultiMapIndexCreationTask +{ + public Animals_ByName() + { + AddMap(cats => from c in cats select new { c.Name }); + AddMap(dogs => from d in dogs select new { d.Name }); + } +} +``` + +
+ + + +RVN005 does not verify that every constructor or execution path registers a map. + +**RVN005 can be suppressed even when no maps are registered at runtime:** + +- A call in any constructor counts, including a base constructor that the index never calls. +- A call inside a condition or loop counts even when the condition prevents it from executing or the loop runs zero times. +- A call inside a lambda or local function nested in a checked constructor or method counts even if the lambda or local function is never invoked. +- Calls to helper methods are matched by name only. A registration in another overload with the same name can therefore suppress RVN005 even when that overload is never called. +- An `AddMapForAll` call counts even if it finds no eligible concrete types and registers no maps. + +**RVN005 can be reported even when maps are registered at runtime:** + +- A registration made only in a property setter is not detected, even if a constructor sets that property. +- A registration made only in a helper invoked through a delegate, such as `Action configure = Configure; configure();`, is not detected. +- A helper method defined in a base class from a referenced project is not followed when only a derived constructor calls it. + `AddMap` or `AddMapForAll` calls reachable from that base class's own constructors still count through its generated index metadata. + + + + + +* [RVN004](./RVN004.mdx) + Equivalent rule for regular single-map indexes. + +* [RVN006](./RVN006.mdx) + Suggests a regular index when only one `AddMap` call is found. + Calls inside loops and indexes using `AddMapForAll` are excluded. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN006.mdx b/docs/client-api/csharp-code-analyzers/RVN006.mdx new file mode 100644 index 0000000000..fe179159ba --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN006.mdx @@ -0,0 +1,237 @@ +--- +title: "RVN006 - Multi-map index uses only a single AddMap" +sidebar_label: RVN006 +sidebar_position: 6 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Multi-map indexes" + link: "indexes/multi-map-indexes" + source: "docs" + path: "Indexes" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" + - title: "Indexing time series" + link: "document-extensions/timeseries/indexing" + source: "docs" + path: "Document Extensions > Time Series" + - title: "Indexing counters" + link: "document-extensions/counters/indexing" + source: "docs" + path: "Document Extensions > Counters" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN006** suggests a single-map base class when it finds exactly one `AddMap` call site in a multi-map index. + Review the index's behavior and configuration before converting it. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN006 reported?](#when-is-rvn006-reported) + * [When is RVN006 not reported?](#when-is-rvn006-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN006)](#before-triggers-rvn006) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +Multi-map index base classes (`AbstractMultiMapIndexCreationTask` +and the time-series and counters variants: `AbstractMultiMapTimeSeriesIndexCreationTask`, `AbstractMultiMapCountersIndexCreationTask`) +support multiple map definitions, including maps over different collections. + +When the index registers only one map, a matching single-map base class can simplify the definition. +Before converting, check that it preserves the index's source scope, result type, and field configuration +(see [How to resolve](#how-to-resolve)). + +RVN006 is a style suggestion: a multi-map index with a single map deploys and works correctly. + + + + + + + +### When is RVN006 reported? + +RVN006 is reported on the class name when exactly one `AddMap` call site is found in the constructors of the index class or its base classes, +including a base class from a referenced project. + +Calls in methods of those classes also count when a constructor calls the method, directly or through other methods. + +RVN006 counts call sites in the bodies it inspects; it does not verify whether each call executes or how often it runs. +See [Limitations](#limitations) for cases where calls are missed or counted without being executed. + + + + +### When is RVN006 not reported? + +- The only call is the multi-map `AddMapForAll` method + (`AddMapForAll("", …)` for time-series and counters indexes). + It registers maps for concrete types assignable to `TBase` in the assembly that defines `TBase`. + This type-based expansion has no direct single-map equivalent. +- The only call is inside a `for`, `foreach`, `while`, or `do` loop in the same constructor or method body, + where it can register a map on each iteration. +- No call is found. [RVN005](./RVN005.mdx) reports this case. +- More than one call to `AddMap` or `AddMapForAll` is found. Both methods count toward the total. + +RVN006 is also not reported for the following index classes: + +- Abstract index classes. Their subclasses are checked instead. +- Indexes that override `CreateIndexDefinition()`. +- Index classes that are `private`, `protected`, or `private protected`, or nested in a class with one of these access levels. + Generic and `protected internal` index classes are checked when the other trigger conditions are met. +- Indexes whose required base-class information cannot be obtained from generated metadata or available source. + This can happen when a base class is in a compiled assembly without the index information recorded by the RavenDB analyzer's source generator. + See [Distribution](./overview.mdx#distribution). + + + + + + + +Consider the matching single-map base class below when the index registers only one map at runtime. +Preserve its result type and field configuration when converting. + +Keep the multi-map base if one call site registers multiple maps, or if the conversion would change the behavior described below +(see also [Limitations](#limitations)). + + + +**Document indexes** + +Replace `AbstractMultiMapIndexCreationTask` with `AbstractIndexCreationTask`. +Assign `Map = …` instead of calling `AddMap(…)`. +Use the same document type as the original `AddMap` type argument for `TDocument`. + + + + +**Time-series indexes** + +Replace `AbstractMultiMapTimeSeriesIndexCreationTask` with `AbstractTimeSeriesIndexCreationTask`. +Keep `AddMap(" + + +**Counters indexes** + +Replace `AbstractMultiMapCountersIndexCreationTask` with `AbstractCountersIndexCreationTask`. +Keep `AddMap("", …)` without the type argument. +Use the original `AddMap` type argument for `TDocument`: the document type whose collection owns the indexed counters. + + + +--- + +**Preserve the result type** + +If the multi-map base specifies a result type, preserve it as the second type argument of the single-map base: ``. +For example, replace `AbstractMultiMapTimeSeriesIndexCreationTask` with `AbstractTimeSeriesIndexCreationTask`. +This applies even when the index does not assign `Reduce`: `Index`, `Store`, `Analyze`, and other typed field options use `TResult`. +For map-reduce indexes, use the two-argument form with the reduce result type as `TResult`. + +--- + +**Time-series and counters: `AddMap`** + +Keep the multi-map base when `AddMap("", …)` selects a named time series or counter across all collections. +The single-map `AddMap(name, …)` method does not support `object`. + +A conversion is possible using `AbstractTimeSeriesIndexCreationTask` or `AbstractCountersIndexCreationTask` +(or the corresponding `` form to preserve a result type), +and the single-map, non-generic `AddMapForAll(…)` method with a name filter inside the map expression. +However, this makes the index process every time series or counter in the database before filtering by name; +the multi-map form selects the named source directly. + +--- + +**Time-series and counters: compound fields** + +Keep the multi-map base if the index uses `CompoundField(…)`. +The current single-map time-series and counters base classes do not copy this configuration into the generated index definition, +so converting silently drops the compound fields. + +--- + +### Before (triggers RVN006) + +```csharp +// Triggers RVN006: only one AddMap call. +public class Orders_ByCompany : AbstractMultiMapIndexCreationTask +{ + public Orders_ByCompany() + { + AddMap(orders => from o in orders select new { o.Company }); + } +} +``` +
+ +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders select new { o.Company }; + } +} +``` + + + + + +RVN006 counts recognized `AddMap` and `AddMapForAll` call sites, not the maps registered at runtime. +A call inside a lambda or local function nested in a checked constructor or method also counts, even if that lambda or local function is never invoked. + +**RVN006 can undercount the maps registered at runtime:** + +- A helper or local function containing one `AddMap` call can run more than once, + for example a generic helper called once per document type. The call site is counted once. +- A helper can be called from a loop while its `AddMap` call is in another method body. + That call is counted once, without the loop exclusion. A single call in a `List.ForEach` callback is also counted once. +- An unqualified `AddMap(…)` call with an inferred type argument is not counted. + The equivalent `this.AddMap(…)` call is recognized. +- A helper defined in a base class from another project is not followed when only a derived constructor calls it. + Registrations reachable from that base class's own constructors still contribute through its recorded information. + +These cases can leave one recognized call and cause RVN006 to be reported even though multiple maps are registered. + +**RVN006 can be suppressed even when only one map is registered:** + +- Calls in different constructors are added together, so two constructors with one `AddMap` call each are not reported. +- Calls in both branches of an `if` are added together, even though only one branch executes. +- Helper methods are matched by name only. Calls in all overloads sharing that name are added together, + including overloads that are never invoked. + + + + + +* [RVN005](./RVN005.mdx) + Reports multi-map indexes for which no `AddMap` or `AddMapForAll` call is found. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN007.mdx b/docs/client-api/csharp-code-analyzers/RVN007.mdx new file mode 100644 index 0000000000..48b9e54675 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN007.mdx @@ -0,0 +1,243 @@ +--- +title: "RVN007 - Query field not defined in the index" +sidebar_label: RVN007 +sidebar_position: 7 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Querying an index" + link: "indexes/querying/query-index" + source: "docs" + path: "Indexes > Querying" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" + - title: "Filter query results" + link: "querying/filtering-query-results/filter-query-results" + source: "docs" + path: "Querying > Filtering Query Results" + - title: "Sort query results" + link: "querying/sorting-query-results/sort-query-results" + source: "docs" + path: "Querying > Sorting Query Results" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN007** reports fields used for filtering, sorting, or searching in a static-index query when the index's map function does not define those fields. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN007 reported?](#when-is-rvn007-reported) + * [When is RVN007 not reported?](#when-is-rvn007-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN007)](#before-triggers-rvn007) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +The map function defines the index-fields in each index entry. +A query on a static index uses these fields for filtering, sorting, and searching. + +If the query references a field the index does not contain, the server rejects the query with an error such as +_"The field '…' is not indexed in '…', cannot query/sort on fields that are not indexed"_. + +RVN007 reports these fields at compile time by comparing the fields referenced in the query with the index-fields defined by the map function. + + + + + + + +### When is RVN007 reported? + +--- + +**Supported queries**: +RVN007 checks queries that target a static index, in sync and async sessions: + +- **By index class:** `session.Query()` + The index class can be defined in the current project or a referenced project. +- **By index name:** `session.Query("IndexName")` or `session.Query(indexName: "IndexName")`, + where the name is a string literal. + The name is matched to an index class in the same project, using the class's `IndexName` override when it returns a string literal, + or the class name with `_` replaced by `/`. + +--- + +**Checked operators**: +RVN007 reports references to fields not defined by the index's map function in the following parts of the query: + + - **Method syntax**: + the first lambda argument of `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, and `Search`. + - **Query syntax**: + the `where` and `orderby` clauses of a query whose first `from` clause is the `Query` call. + +--- + +**How fields are checked**: +RVN007 identifies index-fields from the members of the anonymous object or object initializer in the final projection of each `Map`, `AddMap`, +or `AddMapForAll` expression. The projection can use query syntax (`select`) or method syntax (`Select` or `SelectMany`). +RVN007 reads these expressions from constructor bodies of the index class and its base classes, including classes from referenced projects. + +A field used as the receiver of a method call without a lambda argument, such as `x.Tags.Contains("a")` or `x.Name.StartsWith("A")`, is checked too. +The field in an indexer access, such as `x.Tags[0]`, is also checked. +Each missing field is reported once per lambda, `where` condition, or individual expression in an `orderby` clause. + + + + +### When is RVN007 not reported? + +RVN007 skips some field references and query operators, and is also not reported when the target index or its fields cannot be determined. + +
+Full list of cases not checked + +RVN007 is not reported for the following references and operators: + +- `Id`, which a static-index query can always filter and sort by. +- Nested property paths, such as `x.Address.City` or `x.Name.Length`. +- A collection field used with a lambda operator, such as `x.Lines.Any(l => l.Product == "products/1")`, + as in a fanout index. +- Operators that follow `Select`, `ProjectInto`, `SelectMany`, or `GroupBy` in method syntax, + or that follow `select … into` or `group … into` in query syntax. + +RVN007 is also not reported when the target index or its fields cannot be determined: + +- The index name is not a string literal, does not match an index class in the current project, + or matches more than one index class. +- The index class overrides `IndexName` with a value other than a string literal (string form only). +- The index map uses `CreateField`, `CreateSpatialField`, or `AsJson`, which create field names at runtime. +- A map is not a lambda with a readable body (for example, a block body or a method call), + or its final `select`, `Select`, or `SelectMany` projection is not an anonymous object or object initializer. +- JavaScript indexes, and indexes that override `CreateIndexDefinition()`. +- Single-map time-series and counters indexes. Their multi-map variants are checked. +- Index classes declared `private`, `protected`, or `private protected`, or nested in a class with one of these access levels. +- The index class or a base class cannot be analyzed, for example because it comes from a compiled assembly + without information recorded by the RavenDB source generator. +- Stored-field configuration cannot be analyzed, for example `Store(nameof(Order.Company), FieldStorage.Yes)`. + RVN007 recognizes stored-field names supplied as string literals or supported field lambdas, such as `x => x.Company`. + +
+ +
+ +
+ + + +Define the missing index-field in the map function, or query an index that already defines it. + +--- + +### Before (triggers RVN007) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public class IndexEntry + { + public string Company { get; set; } + public string Employee { get; set; } + } + + public Orders_ByCompany() + { + // Employee is declared in IndexEntry but not set by the map, + // so it is not an index-field. + Map = orders => from o in orders + select new IndexEntry + { + Company = o.Company + }; + } +} + +var results = session.Query() + // Triggers RVN007: Employee is not defined as an index-field. + .Where(x => x.Employee == "employees/1-A") + // Return the matching Order documents. + .OfType() + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields: + public string Company { get; set; } + public string Employee { get; set; } + } + + public Orders_ByCompany() + { + Map = orders => from o in orders + select new IndexEntry + { + Company = o.Company, + Employee = o.Employee + }; + } +} + +var results = session.Query() + .Where(x => x.Employee == "employees/1-A") + // Return the matching Order documents. + .OfType() + .Take(50) + .ToList(); +``` + +
+ + + +- RVN007 reads map fields only from constructor bodies; it does not follow helper methods called by constructors. + If the map is assigned entirely in a helper, RVN007 finds no index-fields and reports every otherwise checked field except `Id`, + even when the map defines those fields. [RVN004](./RVN004.mdx) does not report a missing map in this case, + while [RVN001](./RVN001.mdx) reports the assignment outside the constructor. + +- Only operators in the same method chain starting with the `Query` call are checked. + A query stored in a variable and filtered in a later statement is not checked. + +- RVN007 does not stop at `ToList()` or `AsEnumerable()` when following a method chain. + Supported operators after these calls are still checked even though they run in memory. + The exclusions for operators after a projection or grouping still apply. + +- In query syntax, the first `from` clause must be the `Query` call itself. + A source with other operators applied, such as `session.Query().Customize(…)`, is not checked. + + + + + +* [RVN002](./RVN002.mdx) + Reports query operators placed after a `ProjectInto` or `Select` projection. + +* [RVN008](./RVN008.mdx) + Fires when a projected field is not retrievable from the index or the source document. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN008.mdx b/docs/client-api/csharp-code-analyzers/RVN008.mdx new file mode 100644 index 0000000000..84e4232857 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN008.mdx @@ -0,0 +1,271 @@ +--- +title: "RVN008 - Projected field not retrievable under the applied ProjectionBehavior" +sidebar_label: RVN008 +sidebar_position: 8 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Projection behavior with a static-index" + link: "indexes/querying/projections#projection-behavior-with-a-static-index" + source: "docs" + path: "Indexes > Querying" + - title: "Storing data in index" + link: "indexes/storing-data-in-index" + source: "docs" + path: "Indexes" + - title: "Customize query" + link: "querying/customize-query" + source: "docs" + path: "Querying" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN008** reports fields in a static-index query projection that cannot be retrieved under the query's `ProjectionBehavior`. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN008 reported?](#when-is-rvn008-reported) + * [When is RVN008 not reported?](#when-is-rvn008-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN008)](#before-triggers-rvn008) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +**Field retrieval**: +When a query on a static index projects its results, RavenDB retrieves each projected field according to the query's `ProjectionBehavior`: + - **`Default`**: from the index if the field is stored, otherwise from the source document. + - **`FromIndex`**, **`FromIndexOrThrow`**: only from stored index-fields. + - **`FromDocument`**, **`FromDocumentOrThrow`**: only from the source document. + +For map-reduce indexes, projected fields come from the reduce results. +`FromDocument` and `FromDocumentOrThrow` are not supported; the query fails. + +--- + +**Unavailable fields**: +An unavailable field is skipped with `Default`, `FromIndex`, or `FromDocument`. +With `FromIndexOrThrow` or `FromDocumentOrThrow`, the query throws instead. + +--- + +**What RVN008 checks**: +RVN008 reports these fields at compile time by comparing each projected field with the index's stored fields and with the members of the source document type. + + + + + + + +### When is RVN008 reported? + +RVN008 checks projections on queries that target a static index, in sync and async sessions: + +- **By index class:** `session.Query()` + The index class can be generic and can be defined in the current project or a referenced project. +- **By index name:** `session.Query("IndexName")` or `session.Query(indexName: "IndexName")`, + where the name is a string literal, matched to an index class as described in [RVN007](./RVN007.mdx#when-is-rvn007-reported). + +RVN008 also checks projections applied to a query stored in a local variable, following the query from the variable's declaration initializer. +See [Limitations](#limitations) for subsequent reassignments. + +RVN008 reports projected fields that are not retrievable under the query's `ProjectionBehavior`, +in the following projection forms: + +- **`ProjectInto()`**: + every public instance property and field of `TProjection`, including inherited members. + Reported on the type argument. + +- **`Select` with an anonymous object or object initializer**: + each member whose value is a member of the lambda parameter, such as `x.Company`. + Reported on that member. + +- **Query syntax**: + the final `select` clause, checked the same way as `Select`. + +--- + +**A field is retrievable**: + +- From the index when it is stored: + registered with `Store(…)`, `Stores[…]`, `StoresStrings[…]`, `Stores.Add(…)`, or `StoresStrings.Add(…)` with `FieldStorage.Yes`. + All index-fields count as stored when the index calls `StoreAllFields(FieldStorage.Yes)` or defines a `Reduce` function. + +- From the source document when it is a public instance property or field of the document type the index maps, + the `TDocument` of `AbstractIndexCreationTask`, even when the query uses another type, such as the index's `IndexEntry` class. + +--- + +**Projection behavior** + +RVN008 reads the behavior from `.Customize(x => x.Projection(ProjectionBehavior.…))` calls that precede the projection. +If more than one such call is recognized, the last one before the projection applies. +When no such customization is found, `Default` is assumed. + + + + +### When is RVN008 not reported? + +RVN008 skips some fields and projection forms, and is also not reported when the index, its fields, or the projection behavior cannot be determined. + +
+Full list of cases not checked + +RVN008 is not reported for the following fields and projections: + +- `Id` in a `Select` projection or query-syntax `select`, which is always retrievable. + In `ProjectInto`, `Id` is checked like any other member. +- Constructor projections (`new OrderDto(x.Company)`), single-member selects (`Select(x => x.Company)`), + and members whose value is computed or a nested path, such as `x.Address.City`. +- **Method syntax:** + a projection that follows `Select`, `ProjectInto`, `SelectMany`, `GroupBy`, `OfType`, `Cast`, or `As` in the chain. +- **Query syntax:** + any query expression containing an `into` continuation or ending with a `group … by` clause. +- Queries that do not target a static index. + +RVN008 is also not reported when the index, its fields, or the behavior cannot be determined: + +- Multi-map indexes, which have no single source document type. +- Time-series and counters indexes. +- The `Projection(…)` argument is not written as `ProjectionBehavior.Member`, + for example a variable or a fully qualified enum name. +- A stored-field registration uses a field name other than a string literal or a lambda that selects a direct field, such as `x => x.Company`. + For example, `Store(x => x.Address.City, FieldStorage.Yes)` and `Store(nameof(Order.Company), FieldStorage.Yes)` stop analysis. +- The `StoreAllFields(…)` argument cannot be recognized as `FieldStorage.Yes` or `FieldStorage.No`, + for example when it is supplied through a variable. +- The index name is not a string literal, does not match an index class in the current project, + or matches more than one index class. +- The index class overrides `IndexName` with a value other than a string literal (string form only). +- The index map uses `CreateField`, `CreateSpatialField`, or `AsJson`, which create field names at runtime. +- A map is not an expression-bodied lambda, for example `Map = GetMap()` or a lambda with a block body, + or its final `select`, `Select`, or `SelectMany` projection is not an anonymous object or object initializer. +- JavaScript indexes, and indexes that override `CreateIndexDefinition()`. +- Index classes declared `private`, `protected`, or `private protected`, or nested in a class with one of these access levels. +- The index class or a base class cannot be analyzed, for example because it comes from a compiled assembly + without information recorded by the RavenDB source generator. + +
+ +
+ +
+ + + +Make each projected field retrievable under the query's behavior: + +- Store the field in the index (`Default`, `FromIndex`, and `FromIndexOrThrow`). +- Project a field that exists on the source document (`Default`, `FromDocument`, and `FromDocumentOrThrow`). +- Otherwise, remove the field from the projection or change the `ProjectionBehavior`. + +--- + +### Before (triggers RVN008) + +```csharp +public class Orders_WithTotal : AbstractIndexCreationTask +{ + public class IndexEntry + { + public string Company { get; set; } + } + + public Orders_WithTotal() + { + Map = orders => from o in orders + select new IndexEntry + { + Company = o.Company + }; + } +} + +public class OrderDto +{ + public string Company { get; set; } + // Total is not stored in the index and is not a property of Order. + public decimal Total { get; set; } +} + +var results = session.Query() + // Triggers RVN008 on OrderDto: Total is not retrievable. + .ProjectInto() + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +public class Orders_WithTotal : AbstractIndexCreationTask +{ + public class IndexEntry + { + public string Company { get; set; } + public decimal Total { get; set; } + } + + public Orders_WithTotal() + { + Map = orders => from o in orders + select new IndexEntry + { + Company = o.Company, + Total = o.Lines.Sum(l => l.Quantity * l.PricePerUnit * (1 - l.Discount)) + }; + + // Store Total so the projection can retrieve it from the index. + Store(x => x.Total, FieldStorage.Yes); + } +} + +var results = session.Query() + .ProjectInto() + .Take(50) + .ToList(); +``` + +
+ + + +- RVN008 reads index-fields and stored-field registrations only from constructor bodies of the index class and its base classes, + not from helper methods they call. + Maps assigned entirely in a helper and fields stored only in a helper can be missed. + + RVN008 may therefore report a retrievable field under `FromIndex` or `FromIndexOrThrow`, + or under `Default` when the field is not a member of the source document. + +- RVN008 follows local-variable declaration initializers but does not track subsequent reassignments. + Later assignments can therefore make RVN008 check the original query or behavior instead of the one used at runtime. + +- RVN008 only reads expression-bodied `Customize` lambdas whose final call is `Projection(…)`, and only before the query projection. + For example, `x => x.Projection(…).WaitForNonStaleResults()` is not recognized. + An ignored customization can cause RVN008 to assume `Default` or use an earlier recognized behavior. + + + + + +* [RVN007](./RVN007.mdx) + Fires when a query filters, sorts, or searches on a field that the index's map function does not define. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN009.mdx b/docs/client-api/csharp-code-analyzers/RVN009.mdx new file mode 100644 index 0000000000..5f56e961d0 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN009.mdx @@ -0,0 +1,213 @@ +--- +title: "RVN009 - Unsupported method call inside index Map/Reduce expression" +sidebar_label: RVN009 +sidebar_position: 9 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Extending indexes" + link: "indexes/extending-indexes" + source: "docs" + path: "Indexes" + - title: "Additional assemblies" + link: "indexes/additional-assemblies" + source: "docs" + path: "Indexes" + - title: "Indexing LINQ extensions" + link: "indexes/indexing-linq-extensions" + source: "docs" + path: "Indexes" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN009** reports calls to methods declared in your own code inside an index's `Map`, `Reduce`, `AddMap`, + or `AddMapForAll` lambda. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN009 reported?](#when-is-rvn009-reported) + * [When is RVN009 not reported?](#when-is-rvn009-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN009)](#before-triggers-rvn009) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +An index's `Map` and `Reduce` expressions run on the server, not in your application. +The client sends them to the server as C# source text, and the server compiles that text when the index is deployed. + +--- + +**Calls to custom methods**: + +Methods declared in your own code are not part of that source, so the server cannot call them. +Depending on the call, one of the following happens: + +- The server fails to compile the index, and deploying it throws `IndexCompilationException`. + For example, a call to a static helper such as `TextHelpers.Normalize(p.Name)`. + +- The index compiles, but your method never runs: + on the server, documents are dynamic JSON objects, not instances of your classes. + For example, suppose your `Product` class defines a custom `ComputeKey()` method that calculates a value from the product's properties. + Calling `p.ComputeKey()` in the map, where `p` represents a product document, does not execute that method. + The index field does not get the value your method would compute. + +--- + +**Supported methods**: + +The server can compile LINQ operators, standard .NET methods such as `string` and `Math` methods, +and RavenDB indexing methods such as `LoadDocument` and `CreateField`. + +To run your own logic on the server, you can deploy a static helper with the index using `AdditionalSources` or `AdditionalAssemblies`. + + + + + + + +### When is RVN009 reported? + +RVN009 checks the lambdas that define the index in the constructors of the index class: + +- **`Map` and `Reduce` assignments** (including `this.Map = …` and `base.Reduce = …`), + in document indexes that derive from `AbstractIndexCreationTask`, `AbstractIndexCreationTask`, or `AbstractMultiMapIndexCreationTask`. + `Reduce` assignments in multi-map time-series and counters indexes are also checked. + +- **`AddMap` and `AddMapForAll` calls**, in multi-map indexes. + +In these lambdas, including nested lambdas, RVN009 reports each call to a method +declared in the source code of the project that contains the index: + +- Static methods, such as a helper in a utility class or in the index class itself. +- Instance methods of your own types, such as a method of the document class. +- Extension methods you declared. +- User-written overrides of `ToString()`, `Equals(object)`, and `GetHashCode()`. + When called on a document, these run on the server's JSON object, so your override is not used. + For example, `p.ToString()` returns the document's JSON text. + +Reported on the method name. + + + + +### When is RVN009 not reported? + +- The index class, or one of its base classes, writes to `AdditionalSources` or `AdditionalAssemblies`: + assigns the property, sets an entry (`AdditionalSources["Key"] = …`), or calls `Add(…)` on it. + RVN009 then assumes the server can compile the methods the index calls, and reports nothing for that index. + Only reading these properties does not count. + RVN009 does not verify that the additional code actually provides the called methods. +- Members generated by the compiler, such as the `Equals` and `ToString` methods of a record. +- Delegate invocations, such as calling a `Func<…>`. +- Calls outside the `Map`, `Reduce`, `AddMap`, and `AddMapForAll` lambdas. +- JavaScript indexes (`AbstractJavaScriptIndexCreationTask`), which define their maps as strings. + + + + + + + +Do one of the following: + +- Inline the method's logic in the lambda, using only constructs the server can compile. +- Use a static helper that takes the document values it needs as parameters, for example `KeyHelpers.ComputeKey(p.Name)`, + and deploy the helper with the index using [AdditionalSources](../../indexes/extending-indexes.mdx#including-additional-sources-from-client-code) + or [AdditionalAssemblies](../../indexes/additional-assemblies.mdx). + +Deploying the document class itself does not make calls to its instance methods, such as `p.ComputeKey()`, work. +On the server, `p` is a dynamic JSON object, not an instance of your class. + +When RVN009 detects a write to `AdditionalSources` or `AdditionalAssemblies`, it suppresses diagnostics for the whole index. +This can hide calls such as `p.ComputeKey()` that still do not run your method. + +--- + +### Before (triggers RVN009) + +```csharp +public static class TextHelpers +{ + public static string Normalize(string s) => s.Trim().ToLower(); +} + +public class Products_ByName : AbstractIndexCreationTask +{ + public Products_ByName() + { + Map = products => from p in products + // Triggers RVN009: Normalize is declared in your code. + select new { Name = TextHelpers.Normalize(p.Name) }; + } +} +``` +
+ +### After (correct) + +```csharp +public class Products_ByName : AbstractIndexCreationTask +{ + public Products_ByName() + { + Map = products => from p in products + select new { Name = p.Name.Trim().ToLower() }; + } +} +``` + +
+ + + +- RVN009 checks only lambdas in constructor bodies. + Lambdas in `Map` or `Reduce` assignments and `AddMap` or `AddMapForAll` calls in a separate method are not checked, + even when a constructor calls that method. + + See [RVN001](./RVN001.mdx) for `Map` and `Reduce` assignments outside constructors. + RVN001 does not cover `AddMap` or `AddMapForAll` calls. + +- A partial class declaration that lists neither a base class nor any interfaces is skipped. + Lambdas in constructors declared in that part are not checked, even when another part declares the index base class. + +- A lambda stored in a variable and then assigned to `Map` or `Reduce`, or passed to `AddMap` or `AddMapForAll`, is not checked. + For example, RVN009 does not inspect the lambda referenced by `Map = map;`. + +- Methods from compiled assemblies, such as a NuGet package or a referenced DLL, are not reported, + although the server cannot call them either unless the index deploys them using `AdditionalAssemblies`. + +- Single-map time-series and counters indexes are not checked. + +- RVN009 cannot detect `AdditionalSources` or `AdditionalAssemblies` writes in a base class from another assembly + when neither its source nor usable generated index metadata can be read. + In that case, it can report a call even when the base class deploys the called method. + + + + + +* [RVN010](./RVN010.mdx) + The query equivalent: fires when a method declared in your code is called inside a RavenDB query lambda. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN010.mdx b/docs/client-api/csharp-code-analyzers/RVN010.mdx new file mode 100644 index 0000000000..ad88b6e86f --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN010.mdx @@ -0,0 +1,259 @@ +--- +title: "RVN010 - Unsupported method call inside RavenDB query expression" +sidebar_label: RVN010 +sidebar_position: 10 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Custom methods and extensions for LINQ" + link: "querying/overview#custom-methods-and-extensions-for-linq" + source: "docs" + path: "Querying" + - title: "What is RQL" + link: "querying/rql/what-is-rql" + source: "docs" + path: "Querying > RQL" + - title: "Project query results" + link: "client-api/session/querying/how-to-project-query-results" + source: "docs" + path: "Client API > Session > Querying" + - title: "Map indexes" + link: "indexes/map-indexes" + source: "docs" + path: "Indexes" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN010** reports calls to methods declared in your own code inside the lambdas and clauses of a RavenDB LINQ query. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN010 reported?](#when-is-rvn010-reported) + * [When is RVN010 not reported?](#when-is-rvn010-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN010)](#before-triggers-rvn010) + * [After (correct)](#after-correct) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +The RavenDB client translates a LINQ query, including the lambdas passed to `Where`, `OrderBy`, `Select`, and similar methods, into RQL, and sends the RQL to the server. + +The RavenDB client can translate member access, operators, and a set of supported .NET and RavenDB methods, +but does not automatically translate the code inside methods you define. + +* **Calls that need document data**: + For example, `EmployeeHelpers.FullName(e)` calculates a full name from the `FirstName` and `LastName` properties of employee document `e`. + For calls like this, the outcome depends on where the call appears: + + - In a filter or sort, such as `Where` or `OrderBy`, a call without a supported translation causes `NotSupportedException` during client-side query translation. + + - In a projection (`Select`), expressions can be translated to JavaScript that runs on the server. + Such a call can fail during translation or on the server, or behave differently from your C# implementation. + For example, a call to your `ToString()` override becomes JavaScript's `toString()`; your C# override is not executed. + +* **Calls that do not need document data**: + A method call such as `Defaults.Status()` can return a default value, such as `"Active"`, without using data from a document being queried. + In `o.Status == Defaults.Status()`, the condition uses the document's `Status`, but the `Defaults.Status()` call does not depend on `o`. + In this `Where` condition, the client evaluates `Defaults.Status()` before sending the query, and RVN010 does not report the call. + + In a projection, a static call such as `Defaults.Status()` is reported even though it does not use document data. + The call may be translated to JavaScript instead of being evaluated locally, or fail during translation. + + + + + + + +### When is RVN010 reported? + +RVN010 checks calls to methods declared in the source code of the project that contains the query. +Inside a checked lambda or query clause of a RavenDB LINQ query, RVN010 reports such a call when either condition is met: + +- The call references a lambda parameter or range variable declared within the checked lambda or C# query, + either in an argument or in the expression the method is called on. +- The call appears in a projection (a `Select` or `SelectMany` lambda, or a `let` or `select` clause) + and is not an instance call on an object captured from outside the query. + Captured objects include local variables, parameters, and instance fields or properties. + +For example, using `EmployeeHelpers.FullName` from the [Before example](#before-triggers-rvn010) section below: + +```csharp +var query = session.Query() + .Where(e => EmployeeHelpers.FullName(e) == "Nancy Davolio"); +``` +
+ +RVN010 is reported on `FullName`, which is declared in your code and receives `e`. + +Both method syntax and C# query syntax are checked. + +
+Query methods and clauses checked by RVN010 + +**Query methods** + +RVN010 checks lambda arguments passed to: + +- `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending` +- `Select`, `SelectMany`, `GroupBy` +- `Search`, `Filter`, `Spatial`, `VectorSearch` +- `OrderByDistance`, `OrderByDistanceDescending`, `MoreLikeThis` +- `GroupByArrayValues`, `GroupByArrayContent` + +--- + +**C# query clauses** + +RVN010 checks expressions in `where`, `orderby`, `let`, and `select`, +both expressions in `group … by`, and the key expressions on both sides of `equals` in a `join`. +Clauses after an `into` continuation are also checked. + +--- + +**Additional coverage** + +- Static, instance, and extension methods, as well as local functions. + User-defined overrides of `ToString()`, `Equals(object)`, and `GetHashCode()` are also checked. +- Queries in sync and async sessions, including through local variables and after operators such as `OfType()`. +- Calls inside nested lambdas within checked expressions, such as a helper call inside an `Any(…)` predicate. +- Calls inside nested C# queries within checked expressions, such as `from l in o.Lines where Helper(l) select l` inside a projection. + +
+ +
+ + +### When is RVN010 not reported? + +- Outside projections, calls that do not use a lambda parameter or range variable declared in the query, + such as `Defaults.Status()` in `.Where(o => o.Status == Defaults.Status())`. + This also applies to calls that use only a variable from outside the query, even a parameter of an enclosing lambda. +- Inside projections, instance calls on objects captured from outside the query, provided the call does not reference a query lambda parameter or range variable. + For example, `formatter.Label()` is not reported in `Select(o => new { o.Status, Label = formatter.Label() })` + when `formatter` is a local variable, a parameter, or an instance field or property. + A call such as `formatter.Label(o.Status)` is still reported because it uses document data. + The exception also does not apply to calls through static fields or properties, such as `Formatter.Shared.Label()`. +- Calls inside the lambda passed to a time-series `GroupBy`, such as `RavenQuery.TimeSeries(x, "Readings").GroupBy(g => …)`, which the client runs locally. +- Arguments that are not lambdas, such as the coordinates passed to `OrderByDistance`. +- Members generated by the compiler, such as the `Equals` and `ToString` methods of a record. +- Delegate invocations, such as calling a `Func<…>`. +- LINQ over results already loaded into memory, for example after `ToList()`. +- Document queries (`session.Advanced.DocumentQuery()`). + + + +
+ + + +Do one of the following: + +- **Compute the value before the query** if the call does not depend on document data. + Store the result in a local variable and use that variable in the query. For example: + + ```csharp + var status = Defaults.Status(); + var query = session.Query() + .Select(o => new { o.Id, Status = status }); + ``` + +- **Compute the value at indexing time**: + move the logic into the `Map` of a [static index](../../indexes/map-indexes.mdx#combining-multiple-fields), and query the resulting index-field. + The index map runs on the server; see [RVN009](./RVN009.mdx) for the methods it can call. + +- **Rewrite the condition or projection** using document fields and operators the client can translate. + +- **Apply the method on the client** after loading the results. + Filter and limit the query as much as possible first: every result the query returns is loaded into memory + (see [RVN013](./RVN013.mdx)). + +--- + +### Before (triggers RVN010) + +```csharp +public static class EmployeeHelpers +{ + public static string FullName(Employee e) => e.FirstName + " " + e.LastName; +} + +var employees = session.Query() + // Triggers RVN010: FullName is declared in your code. + .Where(e => EmployeeHelpers.FullName(e) == "Nancy Davolio") + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +public class Employees_ByFullName : AbstractIndexCreationTask +{ + public class IndexEntry + { + public string FullName { get; set; } + } + + public Employees_ByFullName() + { + // The full name is computed on the server during indexing. + Map = employees => from e in employees + select new IndexEntry + { + FullName = e.FirstName + " " + e.LastName + }; + } +} + +var employees = session.Query() + .Where(x => x.FullName == "Nancy Davolio") + .OfType() + .Take(50) + .ToList(); +``` + +
+ + + +- Only the methods and clauses listed above are checked. A predicate passed directly to another query operator, + such as `session.Query().FirstOrDefault(o => …)`, or to `First`, `Single`, `Any`, or `Count`, is not checked. + +- Methods from compiled assemblies, such as a NuGet package or a referenced DLL, are not reported. + The absence of RVN010 does not guarantee that such calls can be translated. + +- Custom query translations registered through client conventions are not considered by RVN010. + A method with a registered translation can still be reported. + +- Queries typed as `IRavenQueryable` are recognized directly. + For queries typed as `IQueryable`, RVN010 follows method chains and local-variable initializers to identify a RavenDB query. + It does not follow `IQueryable` parameters, fields, or properties. + + + + + +* [RVN009](./RVN009.mdx) + The index equivalent: fires when a method declared in your code is called inside an index `Map` or `Reduce` lambda. + +* [RVN013](./RVN013.mdx) + Fires when a query result is not bounded by `Take()`. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN011.mdx b/docs/client-api/csharp-code-analyzers/RVN011.mdx new file mode 100644 index 0000000000..88feb04f6a --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN011.mdx @@ -0,0 +1,242 @@ +--- +title: "RVN011 - Use batch.OpenSession inside a subscription Run delegate" +sidebar_label: RVN011 +sidebar_position: 11 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Data subscriptions" + link: "client-api/data-subscriptions/what-are-data-subscriptions" + source: "docs" + path: "Client API > Data Subscriptions" + - title: "How to consume a data subscription" + link: "client-api/data-subscriptions/consumption/how-to-consume-data-subscription" + source: "docs" + path: "Client API > Data Subscriptions > Consumption" + - title: "SubscriptionBatch API" + link: "client-api/data-subscriptions/consumption/api-overview#subscriptionbatcht" + source: "docs" + path: "Client API > Data Subscriptions > Consumption" + - title: "Worker that operates with a session" + link: "client-api/data-subscriptions/consumption/examples#worker-that-operates-with-a-session" + source: "docs" + path: "Client API > Data Subscriptions > Consumption" + - title: "Subscription that uses included documents" + link: "client-api/data-subscriptions/consumption/examples#subscription-that-uses-included-documents" + source: "docs" + path: "Client API > Data Subscriptions > Consumption" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN011** reports sessions opened from the document store, instead of from the batch, inside a subscription worker's `Run` lambda. + +* **Default severity**: _Info_ + **Automated code fix**: _Yes_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN011 reported?](#when-is-rvn011-reported) + * [When is RVN011 not reported?](#when-is-rvn011-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN011)](#before-triggers-rvn011) + * [After (correct)](#after-correct) + * [Automated code fix](#automated-code-fix) + * [How does the code fix rewrite your code?](#how-does-the-code-fix-rewrite-your-code) + * [When is the code fix not offered?](#when-is-the-code-fix-not-offered) + * [What should you check manually?](#what-should-you-check-manually) + * [Limitations](#limitations) + + + + + +A subscription worker passes each batch of documents to the `Run` delegate as a `SubscriptionBatch`. + +--- + +**Sessions opened from the batch**: +A session opened from the batch (`batch.OpenSession()` / `batch.OpenAsyncSession()`) is bound to that batch: + +- It uses the subscription's database and sends its requests to the node the subscription worker is connected to. + `store.OpenSession()` opens a session on the store's default database, which is not necessarily the subscription's database. + +- The batch items are already tracked by the session. + Loading an item by its ID does not send a request to the server, and changes made to `item.Result` are saved when you call `SaveChanges()`. + +- Documents, counters, and time series included by the subscription query are already loaded into the session, + so accessing them does not send a request to the server. + +--- + +**The problem with opening a session from the store**: +A session opened directly from the store is not automatically preloaded with the batch items or included data. +Loading those documents may require additional server requests, and changes made directly to `item.Result` are not automatically tracked by that session. + +--- + +**Tracking and preloading exceptions**: +Nothing is preloaded into a batch session opened with `NoTracking = true`. +If the subscription returns projections or revisions, the items are not tracked; included data is still preloaded. + + + + + + + +### When is RVN011 reported? + +RVN011 is reported when `OpenSession` or `OpenAsyncSession` is called on an `IDocumentStore` (or a type that implements it, such as `DocumentStore`) +inside a lambda passed directly to the `Run` method of a subscription worker (`SubscriptionWorker`). + +This includes calls in a nested lambda, anonymous method, or non-static local function inside the `Run` lambda. +All overloads are reported, including `OpenSession(SessionOptions)` and `OpenSession(string database)`. + +Reported on the method name. + + + + +### When is RVN011 not reported? + +- A batch handler passed as a method group, such as `worker.Run(ProcessBatch)`. +- A lambda stored in a variable and then passed to `Run`, such as `worker.Run(handler)`. +- An anonymous method passed to `Run`, + such as `worker.Run(delegate (SubscriptionBatch batch) { … })`. +- Calls inside a `static` local function within the `Run` lambda, which cannot access the batch parameter. +- Calls outside a subscription worker's `Run` lambda. + + + + + + + +**Use a batch session**: +Open the session from the batch: replace `store.OpenSession()` / `store.OpenAsyncSession()` with `batch.OpenSession()` / `batch.OpenAsyncSession()`, +where `batch` is the parameter of the `Run` lambda. + +**Batch session requirements**: + + - A session can be opened from the batch only **once per batch**; a second call throws `InvalidOperationException`. + If the `Run` delegate opens more than one session, for example a session per item inside a loop, use a single session for the whole batch. + + - The batch session always uses the subscription's database; the batch has no `OpenSession(string database)` overload. + When passing `SessionOptions`, do not set `Database`, `RequestExecutor`, or a `TransactionMode` other than `SingleNode`; + setting any of these throws `InvalidOperationException`. + + - Pass a new `SessionOptions` instance for each batch. Opening a batch session sets its `Database` and `RequestExecutor` properties, + so reusing the instance for another batch throws `InvalidOperationException`. + +**When to keep the store session**: +If an operation intentionally requires another database or a cluster-wide transaction, +keep the store session and suppress RVN011 for that call. +Learn how to suppress RVN011 in [Suppress diagnostics](./overview.mdx#suppress-diagnostics). + +--- + +### Before (triggers RVN011) + +```csharp +worker.Run(async batch => +{ + // Triggers RVN011: the session is opened from the store. + using var session = store.OpenAsyncSession(); + foreach (var item in batch.Items) + { + // Sends a request to the server for each item. + var order = await session.LoadAsync(item.Id); + // ... process order + } + await session.SaveChangesAsync(); +}); +``` +
+ +### After (correct) + +```csharp +worker.Run(async batch => +{ + using var session = batch.OpenAsyncSession(); + foreach (var item in batch.Items) + { + // Served from the session; no request to the server. + var order = await session.LoadAsync(item.Id); + // ... process order + } + await session.SaveChangesAsync(); +}); +``` + +
+ + + + + +### How does the code fix rewrite your code? + +The code fix replaces the store in the reported call with the `Run` lambda's batch parameter, keeping the method name and arguments. +For example, `store.OpenSession(options)` becomes `batch.OpenSession(options)`, +and `store.OpenAsyncSession(options)` becomes `batch.OpenAsyncSession(options)`. + +The code fix uses the actual parameter name, even when it is not named `batch`. + +Applied to the [Before](#before-triggers-rvn011) example, it produces code equivalent to the [After](#after-correct) example. + + + + +### When is the code fix not offered? + +RVN011 can report a call even when the automated code fix is unavailable. +In the following cases, change the code manually: + +- The call is inside a nested lambda or anonymous method within the `Run` lambda, + which may run after the batch is processed. +- The call is inside a loop within the `Run` lambda. +- The `Run` lambda contains more than one call named `OpenSession` or `OpenAsyncSession`, + including calls that already open a session from the batch. +- The call uses the `OpenSession(string database)` or `OpenAsyncSession(string database)` overload, + which the batch does not have. +- `SessionOptions` is passed as a named argument, such as `sessionOptions: …` or `options: …`. +- `SessionOptions` is constructed inline or in a local variable initializer that explicitly assigns + `Database`, `RequestExecutor`, or `TransactionMode`. + This check also rejects explicitly setting `TransactionMode` to `SingleNode`. + + + + +### What should you check manually? + +The code fix does not verify all [batch session constraints](#how-to-resolve) or all effects of tracking batch items. +Before applying the code fix, make sure that: + +- The `Run` delegate opens only one session per batch, including calls made through helper methods + or repeated calls to a local function. +- The `SessionOptions` passed do not set `Database`, `RequestExecutor`, or a `TransactionMode` other than `SingleNode`. + The initializer check does not cover values assigned later or options supplied through a helper method, parameter, field, or property. +- A new `SessionOptions` instance is passed for each batch; an instance used to open a previous batch session + already has `Database` and `RequestExecutor` set. +- Changes made directly to tracked `item.Result` objects are intended to be saved when `SaveChanges()` / `SaveChangesAsync()` is called. +- The code does not store a different object instance under an ID already tracked by the batch session. + For example, `session.Store(newObject, item.Id)` throws `NonUniqueObjectException` while `item.Result` is tracked under `item.Id`. + + + + + + + +- Null-conditional calls are not reported, such as `store?.OpenSession()` or `worker?.Run(…)`. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN012.mdx b/docs/client-api/csharp-code-analyzers/RVN012.mdx new file mode 100644 index 0000000000..d76f4d2c03 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN012.mdx @@ -0,0 +1,351 @@ +--- +title: "RVN012 - Batch independent session operations using the lazy API" +sidebar_label: RVN012 +sidebar_position: 12 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Perform requests lazily" + link: "client-api/session/how-to/perform-operations-lazily#multiple-lazy-requests" + source: "docs" + path: "Client API > Session > How To" + - title: "Perform a lazy query" + link: "client-api/session/querying/how-to-perform-queries-lazily" + source: "docs" + path: "Client API > Session > Querying" + - title: "Loading entities" + link: "client-api/session/loading-entities" + source: "docs" + path: "Client API > Session" + - title: "Reducing server calls (best practices)" + link: "client-api/session/what-is-a-session-and-how-does-it-work#reducing-server-calls-best-practices" + source: "docs" + path: "Client API > Session" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN012** reports independent `Load` calls and materializing queries on the same session that can be sent to the server in a single request using the lazy API. + +* **Default severity**: _Info_ + **Automated code fix**: _Yes_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN012 reported?](#when-is-rvn012-reported) + * [When is RVN012 not reported?](#when-is-rvn012-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN012)](#before-triggers-rvn012) + * [After (correct)](#after-correct) + * [Async session](#async-session) + * [Mixing loads and queries](#mixing-loads-and-queries) + * [Automated code fix](#automated-code-fix) + * [Which operations does the code fix rewrite?](#which-operations-does-the-code-fix-rewrite) + * [How does the code fix rewrite your code?](#how-does-the-code-fix-rewrite-your-code) + * [When is the code fix not offered?](#when-is-the-code-fix-not-offered) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +**Separate requests**: +Each eager `Load` call or materializing query (`ToList`, `ToArray`) can send a separate request to the server. + +**Batching requires independent operations**: +To combine these operations into one request, each operation's inputs (the document ID or IDs for a load, or the query definition) +must be available without waiting for another operation's result. +Operations that meet this condition are called **independent**. + +**Batching with the lazy API**: +When a method performs two or more independent operations on the same session, +they can be registered as lazy operations and sent together in a single **multi-get** request, reducing network round-trips and latency. + + + + + + + +### When is RVN012 reported? + +RVN012 reports two or more independent loads or materializing queries that meet all of the following conditions: + - Use the same session, referenced through a local variable, parameter, or field. + - Occur in the same block and branch within the same method. + See [Operations in separate blocks, branches, or nested functions](#operations-in-separate-blocks-branches-or-nested-functions). + - Use one of the forms listed under [Supported operations](#supported-operations). + - Are not separated by a call that prevents grouping, such as `SaveChanges()`. + See [Calls that separate operations](#calls-that-separate-operations). + +The operations must also be distinct. Repeated loads with the same `T` and either the same constant ID +or the same unmodified local variable or parameter as the ID or IDs argument count as one operation. + +RVN012 is reported on the method name of each qualifying operation. + +--- + +#### Supported operations + +RVN012 checks the following operations in both sync and async sessions: + +- **Loads:** + `session.Load(id)`, `session.Load(ids)`, `session.LoadAsync(id)`, and `session.LoadAsync(ids)`. Only single-argument overloads qualify. + Here, `id` is a single document ID, and `ids` is an `IEnumerable` of document IDs. + +
+ Requirements for load arguments + + The ID or IDs argument must be a literal or a simple identifier referring to one of the following: + - A parameter, field, or property. + - A local variable initialized from a literal or another simple identifier. +
+ + For example, `CustomerId` qualifies, while `this.CustomerId` is treated as dependent. + + The argument must not hold a value taken from another load or query result in the method. + +
+ +- **Queries:** + `ToList()`, `ToArray()`, `ToListAsync()`, and `ToArrayAsync()`, called without arguments on a query that starts from `session.Query()`. + The query can be stored in a local variable, and its chain can include standard `System.Linq` operators such as `Where`. + The query, including its predicates, must not use a value taken from another load or query result in the method. + +
+ + +### When is RVN012 not reported? + +- `First`, `FirstOrDefault`, `Single`, `SingleOrDefault`, `Any`, `Count`, `LongCount`, and their async forms. + RVN012 checks only the query materializers listed under [Supported operations](#supported-operations). +- `Load(id, includes)`, `LoadAsync(id, token)`, + and query materializers called with an argument, such as `ToListAsync(token)`. +- A load whose ID or IDs argument is any other expression, such as string concatenation or interpolation, + a member access like `order.CustomerId`, a method call, or an inline array such as `new[] { "orders/1-A" }`. + These are treated as dependent. A local variable initialized directly with `new[] { ... }` is also excluded. +- Queries via `session.Advanced.DocumentQuery()` or `session.Advanced.RawQuery()`. +- Operations on a session reached through a property or a method call, such as `Session.Load(id)` or `GetSession().Load(id)`. + Only a local variable, a parameter, or a field is treated as a single session instance. + +--- + +#### Operations in separate blocks, branches, or nested functions + +- Operations in separate `if`/`else` branches, `try`/`catch` blocks, loop bodies, `switch` sections, or `switch` expression arms are not grouped. +- Each arm of `?:`, the right-hand side of `??`, `??=`, `&&`, and `||`, and the part after `?.` are also treated as separate branches. +- Operations inside lambdas and anonymous methods are not reported. +- Operations inside local functions are excluded from the enclosing method's group. + +--- + +#### Calls that separate operations + +Operations separated by certain session calls are not grouped together by RVN012. +These intervening calls are called **session barriers**. + +Each side of a barrier is checked separately. +RVN012 can still report a group if either side contains at least two qualifying operations. + +For example: + +```csharp +var order = session.Load("orders/1-A"); + +session.SaveChanges(); + +var product = session.Load("products/1-A"); +``` +
+ +`SaveChanges()` separates these loads into two groups, each containing only one operation. +RVN012 therefore does not report a diagnostic for this example. + +
+Which calls are session barriers? + +The following are barriers: + +- Calls on the session or directly on `session.Advanced`, except for the method names listed below. + Examples include `Store`, `Delete`, `SaveChanges`, `Advanced.Evict`, `Advanced.Clear`, + `Advanced.Refresh`, `Advanced.Exists`, `CountersFor`, and `TimeSeriesFor`. +- Calls that pass the session as an argument. +- A lambda, or a call to a local function, containing one of the above. + +Calls on the session or directly on `session.Advanced` with the following method names are not barriers, +unless the session is passed as an argument: + +- `Load`, `LoadAsync`, `Query`, and `Include`. +- `DocumentQuery`, `AsyncDocumentQuery`, `RawQuery`, and `AsyncRawQuery`. +- `IsLoaded`, `HasChanged`, `WhatChanged`, and `WhatChangedFor`. +- `GetDocumentId`, `GetMetadataFor`, `GetChangeVectorFor`, `GetCountersFor`, `GetTimeSeriesFor`, + `GetLastModifiedFor`, `GetTrackedEntities`, and `GetCurrentSessionNode`. + +Calls on objects reached through `session.Advanced`, such as `Advanced.Lazily`, `Advanced.Eagerly`, +`Advanced.Attachments`, or `Advanced.Revisions`, are not barriers unless the session is passed as an argument. + +A direct call on another session splits only that session's operations. +A lambda or a local function call containing a barrier splits the operations of all sessions. + +
+ +
+ +
+ + + +* Register the operations with `session.Advanced.Lazily.Load(id)` and `query.Lazily()`, + or with `session.Advanced.Lazily.LoadAsync(id)` and `query.LazilyAsync()` in an async session, + then read the values. + +* Reading the first `.Value` (awaiting it in an async session) sends all pending lazy operations to the server in a single request. + +* Calling `session.Advanced.Eagerly.ExecuteAllPendingLazyOperations()` + (or `ExecuteAllPendingLazyOperationsAsync()`) before reading the values is optional. + +* The automated code fix converts eligible loads and queries to lazy operations and adds code to read their values. + See [Automated code fix](#automated-code-fix). + +--- + +### Before (triggers RVN012) + +```csharp +// Triggers RVN012: two independent loads that may send two separate requests. +var order = session.Load("orders/1-A"); +var product = session.Load("products/1-A"); +``` +
+ +### After (correct) + +```csharp +var lazyOrder = session.Advanced.Lazily.Load("orders/1-A"); +var lazyProduct = session.Advanced.Lazily.Load("products/1-A"); + +// Reading the first value sends both loads in a single request. +var order = lazyOrder.Value; +var product = lazyProduct.Value; +``` +
+ +### Async session + +```csharp +var lazyOrder = asyncSession.Advanced.Lazily.LoadAsync("orders/1-A"); +var lazyProduct = asyncSession.Advanced.Lazily.LoadAsync("products/1-A"); + +// Awaiting the first value sends both loads in a single request. +var order = await lazyOrder.Value; +var product = await lazyProduct.Value; +``` +
+ +### Mixing loads and queries + +```csharp +var lazyCompany = session.Advanced.Lazily.Load("companies/1-A"); +var lazyOrders = session.Query() + .Where(x => x.Company == "companies/1-A") + .Take(50) + .Lazily(); + +// Reading the first value sends the load and the query in a single request. +var company = lazyCompany.Value; +var orders = lazyOrders.Value.ToList(); +``` + +
+ + + + + +### Which operations does the code fix rewrite? + +When you apply the code fix to a reported operation, it selects a **group of consecutive declarations** +around that operation in the same block. + +Each declaration must declare one variable whose entire initializer is a single-argument load +or a supported query materializer called without arguments, such as +`var x = session.Load(id);`, `var x = await session.LoadAsync(id);`, or `var xs = session.Query().ToList();`. + +The selection extends both before and after the reported declaration. +It stops at a statement that is not such a declaration, that contains a session barrier, or whose load or query fails +the independence checks described under [Supported operations](#supported-operations). + +Reported operations outside this group, such as operations after a `SaveChanges()` call, are not changed. +Apply the code fix to them separately. + +For example, the code fix batches `order` and `employee` and leaves `company` unchanged after them: + +```csharp +var order = session.Load(orderId); // Reported +var employee = session.Load(employeeId); // Reported +var company = session.Load(order.Company); // Not reported; ends the selection +``` + + + + +### How does the code fix rewrite your code? + +The code fix: + +1. Renames each selected variable, for example from `order` to `lazyOrder`, + and replaces its initializer with `session.Advanced.Lazily.Load(id)` / `LoadAsync(id)`, + or with `query.Lazily()` / `query.LazilyAsync()`. +2. After all lazy registrations, re-declares each original variable with its original type, reading the lazy value: + `lazyX.Value`, `await lazyX.Value`, `lazyXs.Value.ToList()`, or `(await lazyXs.Value).ToList()`. + Array queries use `ToArray()` instead of `ToList()`. +3. Adds `using Raven.Client.Documents;` when a query was rewritten and the namespace is not already in scope. + +Reading the first value (awaiting it for async operations) sends the batch. +The code fix does not add an `ExecuteAllPendingLazyOperations` call. + +Applied to the [Before](#before-triggers-rvn012) example, it produces code equivalent to the [After](#after-correct) example. + + + + +### When is the code fix not offered? + +RVN012 can report operations even when the automated code fix is unavailable. +In the following cases, change the code manually: + +- The reported operation is not the whole initializer of a declaration that declares one variable, + for example when it is assigned to an existing variable, returned, or passed as an argument. +- The selected group has fewer than two operations. +- The group mixes sync and async operations, or uses more than one session. +- An async operation in the group is not directly awaited. + + + + + + + +- Only operations in the same method body are grouped. + Operations split across helper methods are not detected. + +- Dependency tracking does not follow the order of statements. + Once a local variable, parameter, field, or property receives a value taken from a load or query result anywhere in the method, + it is treated as dependent throughout, so a load or query that uses it is not reported, even after it is reassigned to an independent value. + + + + + +* [RVN013](./RVN013.mdx) + Fires when a query result is not bounded by `Take()`, including queries registered with `Lazily()` or `LazilyAsync()`, + such as those produced by the RVN012 code fix. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN013.mdx b/docs/client-api/csharp-code-analyzers/RVN013.mdx new file mode 100644 index 0000000000..64deda02e0 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN013.mdx @@ -0,0 +1,239 @@ +--- +title: "RVN013 - Query result is not bounded by Take()" +sidebar_label: RVN013 +sidebar_position: 13 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Paging query results" + link: "indexes/querying/paging" + source: "docs" + path: "Indexes > Querying" + - title: "Stream query results" + link: "querying/stream-query-results" + source: "docs" + path: "Querying" + - title: "Paging performance hint" + link: "monitoring/alerts-and-notifications/performance-hints#paging" + source: "docs" + path: "Monitoring > Alerts and Notifications" + - title: "Query overview" + link: "querying/overview" + source: "docs" + path: "Querying" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN013** reports RavenDB queries that materialize results with `ToList`, `ToArray`, `ToListAsync`, or `ToArrayAsync`, + or register lazy execution with `Lazily` or `LazilyAsync`, without an explicit result bound. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Trigger condition](#trigger-condition) + * [When is RVN013 reported?](#when-is-rvn013-reported) + * [When is RVN013 not reported?](#when-is-rvn013-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN013)](#before-triggers-rvn013) + * [After (correct)](#after-correct) + * [Document queries and raw queries](#document-queries-and-raw-queries) + * [Retrieving all results](#retrieving-all-results) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +RavenDB applies no default limit to query results. +When a query does not specify a page size, the server returns all matching results in a single call +(see [Paging query results](../../indexes/querying/paging.mdx)). + +Without `Take(n)`, the number of results depends on the data: +a query that returns a few documents against test data can return an entire collection in production, +and `ToList` or `ToArray` loads every result into memory. + +`Lazily()` and `LazilyAsync()` register a query for later execution. +They delay execution but do not limit how many results the query retrieves when it runs. + +By default, when a query returns more than 2048 results, the server raises a ["Page size too big"](../../monitoring/alerts-and-notifications/performance-hints.mdx#paging) performance hint. + + + + + + + +### When is RVN013 reported? + +RVN013 checks calls to `ToList()`, `ToArray()`, `ToListAsync()`, `ToArrayAsync()`, `Lazily()`, and `LazilyAsync()`, +with or without arguments, made on a RavenDB query. +The diagnostic highlights the method name, such as `ToList` or `Lazily`. + +--- + +**Query types checked**: + +- **LINQ queries**: + queries that start from `session.Query()`, in sync and async sessions, + including after standard LINQ operators such as `OfType()`, `Select`, and `GroupBy`. + Queries written with `from … select …` and queries held in local variables are also checked. + +- **Document queries**: + queries that start from `session.Advanced.DocumentQuery()` or `session.Advanced.AsyncDocumentQuery()`. + +- **Raw queries**: + queries that start from `session.Advanced.RawQuery(query)` or `session.Advanced.AsyncRawQuery(query)`, + when the RQL argument is a compile-time string constant that does not contain the word `limit`. + This includes constant interpolated strings, such as `$"from Orders"` or `$"from {Collection}"` when `Collection` is a `const string`. + +--- + +**Where RVN013 looks for `Take()`**: + +The call is reported when the query has no `Take()` call in any of these places: + +- The method chain before the checked call, including the source of a query-syntax `from` clause, + such as `from o in session.Query().Take(10) select o`. +- The initializer of a local variable that holds the query. +- An assignment to that local variable that appears before the checked call in the same method, + such as `query = query.Take(10);`. + +RVN013 treats any `Take()` call in one of these places as an explicit bound, including `Take(int.MaxValue)`. + + + + +### When is RVN013 not reported? + +- `First`, `FirstOrDefault`, `Single`, `SingleOrDefault`, `Any`, `Count`, `LongCount`, and their async forms. + They return a single result or value. +- Raw queries whose RQL text contains the word `limit` (case-insensitive), + or whose RQL argument is not a compile-time string constant, such as a nonconstant variable, a parameter, + a method result, or an interpolated string with a nonconstant hole. +- LINQ over results already loaded into memory, + such as the second `ToList()` in `session.Query().Take(10).ToList().Where(…).ToList()`. + + + + + + + +- In LINQ queries and document queries, call `.Take(n)` before the checked call. +- In raw queries, add a `limit` clause to the RQL. +- To retrieve all results, [page through them](../../indexes/querying/paging.mdx#paging-examples) with `Skip()` and `Take()`, + or [stream](../../querying/stream-query-results.mdx) them instead of loading them all into memory. + If the result set is known to be small, `Take(int.MaxValue)` states that retrieving all results is intentional. + +--- + +### Before (triggers RVN013) + +```csharp +var orders = session.Query() + .Where(x => x.Company == "companies/1-A") + // Triggers RVN013: no Take(), so every matching order is returned. + .ToList(); +``` +
+ +### After (correct) + +```csharp +var orders = session.Query() + .Where(x => x.Company == "companies/1-A") + .Take(50) + .ToList(); +``` +
+ +### Document queries and raw queries + +```csharp +// Document query: bounded by Take() +var orders = session.Advanced.DocumentQuery() + .WhereEquals(x => x.Company, "companies/1-A") + .Take(50) + .ToList(); +``` + +```csharp +// Raw query: bounded by a 'limit' clause in the RQL +var orders = session.Advanced + .RawQuery("from Orders where Company = $company limit 50") + .AddParameter("company", "companies/1-A") + .ToList(); +``` +
+ +### Retrieving all results + +```csharp +var query = session.Query() + .Where(x => x.Company == "companies/1-A"); + +// Stream the results: each result is read from the stream one at a time. +using var stream = session.Advanced.Stream(query); +while (stream.MoveNext()) +{ + Order order = stream.Current.Document; + // Process the order... +} +``` + +```csharp +// The result set is known to be small: state that retrieving all results is intentional. +var orders = session.Query() + .Where(x => x.Company == "companies/1-A") + .Take(int.MaxValue) + .ToList(); +``` + +
+ + + +- Only `ToList`, `ToArray`, `ToListAsync`, `ToArrayAsync`, `Lazily`, and `LazilyAsync` are checked. + Other ways to retrieve all results, such as iterating the query in a `foreach` loop, + `ToDictionary`, or `AsEnumerable().ToList()`, are not reported. + +- A `Take()` applied outside the method is not detected. + A query typed as `IRavenQueryable`, `IDocumentQuery`, or another RavenDB query type is reported when it is received as a parameter, + read from a field or property, or returned by a method, even if it was bounded where it was created. + + A query typed as `IQueryable` is followed only through local-variable initializers; + as a parameter, field, or property, it is not checked. + +- Conditions are not evaluated. + An earlier assignment containing `Take()` inside an `if` block suppresses RVN013 even if the block does not run, + while a `Take()` inside a conditional expression, such as `flag ? query.Take(10) : query`, is not detected. + +- In raw queries, `limit` is matched anywhere in the RQL text, + so a query that uses `limit` only as a field name or inside a string value is not reported. + + Nonconstant interpolated RQL is not checked, even when only a filter value is interpolated. + For example, `$"from Orders where Company = '{company}'"` is not reported when `company` is a nonconstant variable. + + + + + +* [RVN012](./RVN012.mdx) + Reports independent loads and materializing queries on the same session that can be sent in a single request using the lazy API. + + Its code fix converts queries to `Lazily()` or `LazilyAsync()`. RVN013 checks these calls too, + so an unbounded query is still reported at the `Lazily` or `LazilyAsync` method name. + + Add `Take()` before the lazy registration to resolve it. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN014.mdx b/docs/client-api/csharp-code-analyzers/RVN014.mdx new file mode 100644 index 0000000000..5a6505cf89 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN014.mdx @@ -0,0 +1,305 @@ +--- +title: "RVN014 - Index Map fans out over a collection" +sidebar_label: RVN014 +sidebar_position: 14 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Indexing nested data" + link: "indexes/indexing-nested-data#fanout-index---multiple-index-entries-per-document" + source: "docs" + path: "Indexes" + - title: "Indexing performance hints" + link: "monitoring/alerts-and-notifications/performance-hints#indexing" + source: "docs" + path: "Monitoring > Alerts and Notifications" + - title: "Paging through tampered results" + link: "indexes/querying/paging#paging-through-tampered-results" + source: "docs" + path: "Indexes > Querying" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN014** reports index maps that iterate over a nested collection using `SelectMany` or a nested `from` clause, + producing an index-entry for each element of the collection instead of a single index-entry per document. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [What is a fanout index?](#what-is-a-fanout-index) + * [Performance and paging](#performance-and-paging) + * [Runtime performance hint and RVN014](#runtime-performance-hint-and-rvn014) + * [Trigger condition](#trigger-condition) + * [When is RVN014 reported?](#when-is-rvn014-reported) + * [When is RVN014 not reported?](#when-is-rvn014-not-reported) + * [How to resolve](#how-to-resolve) + * [Fanout map (triggers RVN014)](#fanout-map-triggers-rvn014) + * [Intentional fanout (suppress RVN014 with a pragma)](#intentional-fanout-suppress-rvn014-with-a-pragma) + * [Avoid fanout by indexing collection values in a single entry](#avoid-fanout-by-indexing-collection-values-in-a-single-entry) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +#### What is a fanout index? + +An index whose map iterates over a nested collection is a **fanout index**: +it produces an index-entry for each element of the collection, instead of a single index-entry per document. +For example, an index that maps the lines of each order produces one index-entry per order line, +so an order with 50 lines produces 50 index-entries. + +A fanout index is the right choice when a query must match several conditions on the same element, +such as an order line with a specific product _and_ a quantity above 10 (see [RVN015](./RVN015.mdx)). + +--- + +#### Performance and paging + +The cost of a fanout index grows with the size of the collection: + +- Indexing takes longer and uses more CPU and memory, and the index takes more storage. +- When a query returns full documents, `TotalResults` in the query statistics counts the matching index-entries, + not the documents. + Paging must account for the duplicate results the server skips (see [Paging through tampered results](../../indexes/querying/paging.mdx#paging-through-tampered-results)). + +--- + +#### Runtime performance hint and RVN014 + +When a single document produces more index-entries than the [PerformanceHints.Indexing.MaxIndexOutputsPerDocument](../../monitoring/alerts-and-notifications/configuration.mdx#performancehintsindexingmaxindexoutputsperdocument) threshold (`1024` by default), +the server raises a ["High indexing fanout ratio"](../../monitoring/alerts-and-notifications/performance-hints.mdx#indexing) performance hint at runtime. + +RVN014 is reported at compile time when the map contains a fanout pattern detected by the C# analyzer +(see [Trigger condition](#trigger-condition)). + +RVN014 does not check the collection's runtime size, so it is reported even when the server's performance-hint threshold is not exceeded. + + + + + + + +### When is RVN014 reported? + +RVN014 checks the map lambdas in the constructors of the index class: + +- **`Map` assignments** (including `this.Map = …` and `base.Map = …`), in indexes that derive from `AbstractIndexCreationTask` + or `AbstractIndexCreationTask`. +- **`AddMap` and `AddMapForAll` calls**, in multi-map indexes, including multi-map time-series and counters indexes. + +In these lambdas, RVN014 reports: + +- A `SelectMany` call in the lambda's method chain, such as `orders.SelectMany(order => order.Lines)`, + or in the source of a `from` clause, such as `from line in orders.SelectMany(order => order.Lines)`. + Reported on the `SelectMany` method name. +- A `from` clause that follows the first `from` clause of the query expression, such as `from line in order.Lines`, + including a `from` clause that follows an `into` continuation. + This is also reported when the query expression is wrapped in parentheses or followed by a method call, + such as `(from order in orders from line in order.Lines select …).Where(…)`. + Reported on the `from` keyword. + +Each detected fanout operation is reported separately. + + + + +### When is RVN014 not reported? + +- `Reduce` lambdas. A reduce function aggregates the index-entries that the map produced, so it does not add index-entries per document. + +- A query expression or a `SelectMany` call inside the projection, + such as `select new { Products = from line in order.Lines select line.Product }`. + It computes a value for the document's single index-entry. + +- In multi-map time-series indexes, the `from` clause that expands each time-series segment into its entries, + such as `from entry in ts.Entries`. + A further nested `from`, such as `from value in entry.Values`, is reported. + +- JavaScript indexes (`AbstractJavaScriptIndexCreationTask`), which define their maps as strings. + + + + + + + +Decide whether the index needs an index-entry per element: + +- **If the fanout is intentional**, for example to match several conditions on the same element, + make sure the collection size is acceptable for your workload, and suppress RVN014 for that index with + `#pragma warning disable RVN014`. + See [Suppress diagnostics](./overview.mdx#suppress-diagnostics) for how to disable and restore a diagnostic around a specific code block. + + Setting `dotnet_diagnostic.RVN014.severity = none` in `.editorconfig` disables RVN014 for all files matched by that configuration section, + which can also hide unintended fanout maps. + +- **If the fanout is not needed**, produce a single index-entry per document: + + - To query documents by values from a nested collection, index the values as a collection in one index-field, + such as `Products = order.Lines.Select(line => line.Product)`. + A query on that index-field matches a document when any element holds the value, + but conditions on two such index-fields can be matched by different elements. + + - To query by a value computed over the collection, aggregate it, + such as `TotalQuantity = order.Lines.Sum(line => line.Quantity)`. + +--- + +### Fanout map (triggers RVN014) + +```csharp +public class Orders_ByProduct : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public string Product { get; set; } + public int Quantity { get; set; } + } + + public Orders_ByProduct() + { + // Triggers RVN014: SelectMany produces an index-entry per order line. + Map = orders => orders.SelectMany( + order => order.Lines, + (order, line) => new IndexEntry + { + Product = line.Product, + Quantity = line.Quantity + }); + } +} +``` + +```csharp +public class Orders_ByProduct : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public string Product { get; set; } + public int Quantity { get; set; } + } + + public Orders_ByProduct() + { + Map = orders => from order in orders + // Triggers RVN014: the nested 'from' produces an index-entry per order line. + from line in order.Lines + select new IndexEntry + { + Product = line.Product, + Quantity = line.Quantity + }; + } +} +``` +
+ +### Intentional fanout (suppress RVN014 with a pragma) + +```csharp +public class Orders_ByProductAndQuantity : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public string Product { get; set; } + public int Quantity { get; set; } + } + + public Orders_ByProductAndQuantity() + { + // The fanout is intentional: each order line is a separate index-entry, + // so a query can match the product and the quantity on the same line. + // Suppress RVN014 for this intentional fanout map. + #pragma warning disable RVN014 + Map = orders => from order in orders + from line in order.Lines + select new IndexEntry + { + Product = line.Product, + Quantity = line.Quantity + }; + #pragma warning restore RVN014 + } +} +``` + +```csharp +// Orders that have a line with more than 10 units of products/1-A +var orders = session + .Query() + .Where(x => x.Product == "products/1-A" && x.Quantity > 10) + .OfType() + .Take(50) + .ToList(); +``` +
+ +### Avoid fanout by indexing collection values in a single entry + +```csharp +public class Orders_ByProducts : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public IEnumerable Products { get; set; } + } + + public Orders_ByProducts() + { + // A single index-entry per order: + // the 'Products' index-field holds the products of all order lines. + Map = orders => from order in orders + select new IndexEntry + { + Products = order.Lines.Select(line => line.Product) + }; + } +} +``` + +```csharp +// Orders that have a line with products/1-A +var orders = session + .Query() + .Where(x => x.Products.Contains("products/1-A")) + .OfType() + .Take(50) + .ToList(); +``` + +
+ + + +- RVN014 checks only lambdas in constructor bodies. + A `Map` assigned in a method is not checked (see [RVN001](./RVN001.mdx)). + +- The map lambda must appear directly in the `Map` assignment or the `AddMap`/`AddMapForAll` call. + A map expression stored in a variable and then assigned or passed to the call is not checked. + +- Single-map time-series and counters indexes are not checked. + + + + + +* [RVN015](./RVN015.mdx) + Dynamic queries that need a fanout index to match conditions on the same element of a nested collection. + + diff --git a/docs/client-api/csharp-code-analyzers/RVN015.mdx b/docs/client-api/csharp-code-analyzers/RVN015.mdx new file mode 100644 index 0000000000..9ef46d8bf7 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/RVN015.mdx @@ -0,0 +1,353 @@ +--- +title: "RVN015 - Correlated conditions over a nested collection in a dynamic query" +sidebar_label: RVN015 +sidebar_position: 15 +see_also: + - title: "C# code analyzers overview" + link: "client-api/csharp-code-analyzers/overview" + source: "docs" + path: "Client API > C# Code Analyzers" + - title: "Indexing nested data" + link: "indexes/indexing-nested-data#fanout-index---multiple-index-entries-per-document" + source: "docs" + path: "Indexes" + - title: "Filter by values in a collection" + link: "querying/filtering-query-results/filter-query-results#filter-by-values-in-a-collection" + source: "docs" + path: "Querying > Filtering Query Results" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* **RVN015** reports dynamic queries that use `Any` to check whether one item in a nested collection satisfies two or more conditions joined with `&&`. + The query asks for the same item to satisfy all the conditions, but an auto-index can match each condition against a different item. + + RVN015 also reports `All` over a nested collection, because the client does not support `All` inside a query predicate. + +* **Default severity**: _Info_ + **Automated code fix**: _No_ + +* In this article: + * [Description](#description) + * [Indexing nested collections](#indexing-nested-collections) + * [Matching different elements](#matching-different-elements) + * [Runtime behavior](#runtime-behavior) + * [Trigger condition](#trigger-condition) + * [When is RVN015 reported?](#when-is-rvn015-reported) + * [When is RVN015 not reported?](#when-is-rvn015-not-reported) + * [How to resolve](#how-to-resolve) + * [Before (triggers RVN015)](#before-triggers-rvn015) + * [After (correct)](#after-correct) + * [Conditions that can match different elements](#conditions-that-can-match-different-elements) + * [When every element must match](#when-every-element-must-match) + * [Limitations](#limitations) + * [Related C# analyzers](#related-c-analyzers) + + + + + +A query that does not specify an index, such as `session.Query()`, is a dynamic query. +Filtering with `Where` on fields other than the document ID, or sorting with `OrderBy`, uses an [auto-index](../../querying/overview.mdx#2-query-a-collection---with-filtering). +RavenDB reuses a suitable existing auto-index or creates one if needed. + +--- + +#### Indexing nested collections: + +For the nested-collection queries discussed here, the auto-index produces one index-entry per document. +The auto-index indexes a nested collection as one multi-valued index-field per property of the collection element. + +For the `Lines` collection of an order, `Lines[].Product` holds the products of all order lines, and `Lines[].Quantity` holds their quantities. +The index-entry does not keep track of which values belong to the same order line. + +--- + +#### Matching different elements: + +As a result, the auto-index can match the conditions in an `Any` predicate independently. + +For `x.Lines.Any(line => line.Product == "products/1-A" && line.Quantity > 10)`, +the index would match an order when _some_ line has the product and _some_ line, possibly a different one, has more than 10 units. + +To match all conditions on the same element, query a static fanout index, which produces an index-entry per element. +The server does not create fanout auto-indexes. + +--- + +#### Runtime behavior: + +The client handles `Any` and `All` differently: + +- **`Any`: conditions on different properties of the element**: + for the example above, the client throws `InvalidOperationException`: + _"Using multiple fields inside method 'Any' can lead to unexpected query results for auto index. + A static fanout index should be used instead."_ + + RVN015 reports the query at compile time, before it runs. + + The client does not reject every predicate that combines conditions on different properties. + For example, it accepts `(line.Product == "products/1-A" || line.Product == "products/2-A") && line.Quantity > 10` inside `Any`. + Such a query runs, and different lines can satisfy the product and quantity conditions. + + The client also does not throw for conditions on the `Key` and `Value` of a dictionary entry. + For example, `x.Counters.Any(kv => kv.Key == "a" && kv.Value == 1)` + can match a dictionary containing `{ "a": 2, "b": 1 }`: + the key matches one entry and the value matches another. RVN015 warns about this at compile time. + +- **`Any`: non-range conditions on the same property**, + such as `line.ProductName.StartsWith("Ch") && line.ProductName.EndsWith("ai")`: + the client does not throw, and the query can return orders in which different lines satisfy the two conditions. + +- **`Any`: ranges on one property**: + an inclusive range such as `line.Quantity >= 5 && line.Quantity <= 10` is sent as one `between` condition. + A single indexed value must satisfy both bounds. RVN015 counts this range as one condition, so the range alone is not reported. + + Ranges with an exclusive bound, such as `line.Quantity > 5 && line.Quantity < 10` or `line.Quantity >= 5 && line.Quantity < 10`, are still reported. + Depending on the generated RQL grouping, the server may combine these bounds into one range, + making the diagnostic a false positive (see [Limitations](#limitations)). + +- **`All` over a nested collection**: + the client throws `NotSupportedException` (`Method not supported: All`) when the query is executed, + regardless of the predicate. + This also applies to queries over a static index. RVN015 reports this unsupported operation in dynamic queries. + + + + + + + +### When is RVN015 reported? + +RVN015 checks dynamic queries that start from `session.Query()` without specifying an index, in sync and async sessions, +including after standard LINQ operators such as `OfType()`, and through a local variable that holds the query. +Passing only a collection name, such as `session.Query(collectionName: "Orders")`, +still creates a dynamic query and is checked. + +RVN015 checks predicates passed to: + +- `Where`. +- Predicate overloads of `First`, `FirstOrDefault`, `Single`, `SingleOrDefault`, `Any`, `Count`, and `LongCount`. +- Predicate overloads of `FirstAsync`, `FirstOrDefaultAsync`, `SingleAsync`, `SingleOrDefaultAsync`, `AnyAsync`, and `CountAsync`. + +It also checks `where` clauses in query syntax, including clauses after an `into` continuation. + +In these predicates, RVN015 reports an `Any` call when: + + - `Any` is called on a collection reached through the properties of the queried document, such as `x.Lines`, + or through the properties of an element of another collection in the same predicate. + + - The `Any` predicate is a lambda that joins two or more conditions with `&&`, + and at least two of these conditions use the lambda parameter (the collection element). + Conditions that do not use the element, such as a captured variable, are not counted. + An inclusive range on one property, such as `line.Quantity >= 5 && line.Quantity <= 10`, + counts as one condition. + The range alone is not reported, but combining it with another element-dependent condition can trigger RVN015. + +Each `Any` call is checked separately, including an `Any` call nested in the predicate of another `Any` call, +such as `m.Permissions.Any(…)` inside `x.Members.Any(m => …)`. + +RVN015 also reports `All` on a collection reached in the same way, regardless of the predicate or the number of conditions. +It has a separate diagnostic message explaining that `All` is unsupported inside a query predicate. + +Reported on the `Any` or `All` method name. + + + + +### When is RVN015 not reported? + +- An `Any` predicate with a single condition on the element, such as `x.Lines.Any(line => line.Product == "products/1-A")`. +- An `Any` predicate consisting only of an inclusive range on one property, such as `x.Lines.Any(line => line.Quantity >= 5 && line.Quantity <= 10)`. +- An `Any` predicate such as `Any(line => A || B)` whose conditions are joined only by `||`. + This matches the same documents as `Any(line => A) || Any(line => B)`, + so matching the conditions independently returns the correct results. +- Separate `Any` calls, which state that each condition can be matched by a different element, + such as `x.Lines.Any(line => line.Product == "products/1-A") && x.Lines.Any(line => line.Quantity > 10)`. +- An `Any` predicate with only one element-dependent condition and additional conditions on captured values, + such as `x.Lines.Any(line => line.Product == productId && includeAll)`. +- `Any` or `All` over a value that is not a collection of the queried document, such as a string, or a local list (`productIds.Any(…)`). +- Queries over a static index: `session.Query()` or `session.Query(indexName)`. + For an `Any` predicate, whether the conditions are matched on the same element depends on the index map: + a fanout map matches them on the same element, while a map that indexes the collection values in the index-fields of a single index-entry matches them independently + (see [Indexing nested data](../../indexes/indexing-nested-data.mdx)). +- Document queries (`session.Advanced.DocumentQuery()`) and raw queries (`session.Advanced.RawQuery()`). + The client’s check for multiple fields inside a LINQ `Any` predicate does not apply to these APIs. + When these queries filter nested collection fields through an auto-index, different elements can satisfy different conditions. + + + + + + + +For an `Any` diagnostic, decide whether the conditions must be matched on the same element. +For an `All` diagnostic, see [When every element must match](#when-every-element-must-match). + +- **To match all conditions on the same element**, query a static fanout index, whose map produces an index-entry per element of the collection. + + Filter by the index-fields of the element, and use `OfType()` to return the documents. + The fanout map triggers [RVN014](./RVN014.mdx). + + When the fanout is intentional, suppress RVN014 for that index with `#pragma warning disable RVN014`. + See [Suppress diagnostics](./overview.mdx#suppress-diagnostics) for how to disable and restore a diagnostic around a specific code block. + +- **If each condition can be matched by a different element**, use a separate `Any` call for each condition. + The query then states the intended matching explicitly, and the client does not throw. + +--- + +### Before (triggers RVN015) + +```csharp +// Orders that have a line with more than 10 units of products/1-A +var orders = session.Query() + // Triggers RVN015: Any joins two conditions on the collection element with &&. + // When the query is executed, the client throws InvalidOperationException. + .Where(x => x.Lines.Any(line => line.Product == "products/1-A" && line.Quantity > 10)) + .Take(50) + .ToList(); +``` + +```csharp +// Query syntax is reported the same way +var orders = (from x in session.Query() + // Triggers RVN015 + where x.Lines.Any(line => line.Product == "products/1-A" && line.Quantity > 10) + select x) + .Take(50) + .ToList(); +``` +
+ +### After (correct) + +```csharp +public class Orders_ByProductAndQuantity : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public string Product { get; set; } + public int Quantity { get; set; } + } + + public Orders_ByProductAndQuantity() + { + // The fanout is intentional: each order line is a separate index-entry, + // so a query can match the product and the quantity on the same line. + // Suppress RVN014 for this intentional fanout map. + #pragma warning disable RVN014 + Map = orders => from order in orders + from line in order.Lines + select new IndexEntry + { + Product = line.Product, + Quantity = line.Quantity + }; + #pragma warning restore RVN014 + } +} +``` + +```csharp +// Orders that have a line with more than 10 units of products/1-A +var orders = session + .Query() + .Where(x => x.Product == "products/1-A" && x.Quantity > 10) + .OfType() + .Take(50) + .ToList(); +``` +
+ +### Conditions that can match different elements + +If the product and quantity conditions are allowed to match different order lines, use separate `Any` calls: + +```csharp +// Orders that have a line with products/1-A, +// and a line (the same line or another one) with more than 10 units +var orders = session.Query() + .Where(x => x.Lines.Any(line => line.Product == "products/1-A") && + x.Lines.Any(line => line.Quantity > 10)) + .Take(50) + .ToList(); +``` + +
+ +### When every element must match + +`All` requires every element of the collection to satisfy the predicate. +Filtering the per-element fields of a fanout index finds documents with a matching element; it does not establish that every element matches. + +For a fixed requirement, compute a boolean in a static index with one index-entry per document, then query that field. +For example, to find orders where every line has a quantity above 10: + +```csharp +public class Orders_ByAllLineQuantities : AbstractIndexCreationTask +{ + public class IndexEntry + { + // The index-fields + public bool AllLinesAboveTen { get; set; } + } + + public Orders_ByAllLineQuantities() + { + Map = orders => from order in orders + select new IndexEntry + { + AllLinesAboveTen = order.Lines.All(line => line.Quantity > 10) + }; + } +} +``` + +```csharp +// Orders where every line has more than 10 units +var orders = session + .Query() + .Where(x => x.AllLinesAboveTen) + .OfType() + .Take(50) + .ToList(); +``` + +`All` is used in the index map to compute the field; the query predicate tests the boolean. +As with LINQ `All`, an order with an empty `Lines` collection also matches. + +
+ + + +- The predicate passed to a checked query operator must be a lambda written in the call. + A predicate passed as a variable, such as `Where(filter)` where `filter` is an `Expression>`, is not checked. + +- The query must start from `session.Query()` in the same method, + either in the method chain or in the initializer of a local variable that holds the query. + A query received as a parameter, or read from a field or property, is not checked. + +- A range on one property with at least one exclusive bound, such as `line.Quantity > 5 && line.Quantity < 10` + or `line.Quantity >= 5 && line.Quantity < 10`, is reported. + The client sends two comparisons, and the server combines them into one range only when the comparisons form their own `and` group in the generated RQL. + When the bounds are combined, a single indexed value must satisfy both, so RVN015 is a false positive. + This happens, for example, when the range is the only condition in `Where`, or is placed in a separate chained `Where` call. + + Other arrangements can leave the comparisons separate. + For example, in `Where(x => x.Company == "companies/1-A" && x.Lines.Any(line => line.Quantity > 5 && line.Quantity < 10))`, different lines can satisfy the two bounds. + + + + + +* [RVN014](./RVN014.mdx) + Fires on index maps that fan out over a collection, including a fanout index created to resolve RVN015. + + diff --git a/docs/client-api/csharp-code-analyzers/_category_.json b/docs/client-api/csharp-code-analyzers/_category_.json new file mode 100644 index 0000000000..1cef4849b4 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/_category_.json @@ -0,0 +1 @@ +{ "position": 20, "label": "C# Code Analyzers" } diff --git a/docs/client-api/csharp-code-analyzers/overview.mdx b/docs/client-api/csharp-code-analyzers/overview.mdx new file mode 100644 index 0000000000..1a37881d44 --- /dev/null +++ b/docs/client-api/csharp-code-analyzers/overview.mdx @@ -0,0 +1,204 @@ +--- +title: "C# Code Analyzers - Overview" +sidebar_label: Overview +sidebar_position: 0 +see_also: + - title: "Query Overview" + link: "querying/overview" + source: "docs" + path: "Querying" + - title: "Performing Operations Lazily" + link: "client-api/session/how-to/perform-operations-lazily" + source: "docs" + path: "Client API > Session > How To" + - title: "Data Subscriptions" + link: "client-api/data-subscriptions/what-are-data-subscriptions" + source: "docs" + path: "Client API > Data Subscriptions" +--- + +import Admonition from '@theme/Admonition'; +import Panel from "@site/src/components/Panel"; +import ContentFrame from "@site/src/components/ContentFrame"; + + + +* RavenDB ships a set of C# code analyzers embedded in the `RavenDB.Client` NuGet package. + They inspect client code at compile time, identifying common misuse patterns before the application runs. + +* These **C# code analyzers** are distinct from the **text analyzers** used in index definitions for full-text search. + For tokenizers, stemmers, and language analyzers, see [Using Analyzers in Indexes](../../indexes/using-analyzers.mdx). + +* In this article: + * [Installation and requirements](#installation-and-requirements) + * [Diagnostic severity](#diagnostic-severity) + * [Suppress diagnostics](#suppress-diagnostics) + * [Apply automated code fixes](#apply-automated-code-fixes) + * [Available rules](#available-rules) + + + + + + + +The analyzer assembly (`Raven.Analyzers.dll`) and code-fix assembly (`Raven.Analyzers.CodeFixes.dll`) are included in `RavenDB.Client.nupkg` under `analyzers/dotnet/cs/`. +No separate install is required. + +The analyzers require Roslyn 4.4.0 or later, included in the .NET 7 SDK and Visual Studio 2022 version 17.4, and in later versions. + +Older compiler hosts cannot load the analyzers and may report `CS9057`. +This warning can fail the build when `TreatWarningsAsErrors` is enabled. + +To use the client package without loading the analyzers, see [Disable all RavenDB analyzers for a project](#disable-all-ravendb-analyzers-for-a-project). + +Unless excluded, the analyzers run automatically on every build once the client package is referenced. +See [Apply automated code fixes](#apply-automated-code-fixes) for applying fixes in the IDE or from the command line. + +Some rules also inspect indexes defined in referenced assemblies. The checks performed by RVN004–RVN008 are skipped when the index information they require cannot be obtained from generated metadata or available source. + + + + + +New rules default to `Info` severity in the client release that introduces them. +For the rules listed in this article, the default remains `Info` throughout the 7.2.x release line. + +Diagnostics reported at `Info` appear in the IDE but are not printed in `dotnet build` output. +They do not fail builds, even with `TreatWarningsAsErrors` enabled. + +Starting with the next major or minor client release after 7.2, these rules default to `Warning`, +except RVN006, RVN007, and RVN008, which remain at `Info`. + +If `TreatWarningsAsErrors` is enabled, diagnostics reported as `Warning` can fail the build. +Address or suppress relevant occurrences before upgrading to a client version where these rules default to `Warning`. + +You can override the default severity of individual rules in `.editorconfig`: + +```ini +[*.cs] +dotnet_diagnostic.RVN012.severity = warning +dotnet_diagnostic.RVN013.severity = error +``` + + + + + +You can suppress individual RavenDB analyzer rules in a code region or matching files, +or disable all RavenDB analyzers for a project. + + + +### Suppress a diagnostic in a code region + +Use `#pragma warning disable` followed by the rule ID before the relevant code, +and `#pragma warning restore` followed by the same ID after the code: + +```csharp +#pragma warning disable RVN014 +// Code where RVN014 is intentionally suppressed. +#pragma warning restore RVN014 +``` +
+ +The suppression starts on the line after `disable` and lasts until `restore` or the end of the source file. +The `restore` directive restores the rule's configured reporting behavior from the following line. + +Replace `RVN014` with the ID of any RavenDB analyzer rule. +To suppress multiple rules, list their IDs separated by commas in both directives, such as `RVN009, RVN014`. + +For more information, see the [C# pragma warning reference](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#pragma-warning). + +
+ + +### Suppress a diagnostic in matching files + +Set the rule's severity to `none` in `.editorconfig`: + +```ini +[*.cs] +dotnet_diagnostic.RVN014.severity = none +``` +
+ +The rule is suppressed in all files matched by that configuration section. + +
+ + +### Disable all RavenDB analyzers for a project + +To skip loading all analyzers supplied by `RavenDB.Client`, add `ExcludeAssets="analyzers"` to its `PackageReference` in the project file. +Keep your existing package version: + +```xml + +``` + + + +
+ + + +Automated code fixes are suggested code changes that you can choose to apply. +RVN011 and RVN012 provide fixes for supported code patterns. +A diagnostic can still be reported when no automated fix is available; in that case, change the code manually. + + + +### Rules with automated code fixes + +- **[RVN011](./RVN011.mdx)** + Rewrites `store.OpenSession()` / `store.OpenAsyncSession()` to `batch.OpenSession()` / `batch.OpenAsyncSession()` inside a subscription `Run` lambda. + +- **[RVN012](./RVN012.mdx)** + Rewrites independent eager `Load` calls and queries materialized with `ToList` or `ToArray` (including supported async forms) + into lazy registrations whose results are read through `.Value` (awaited for async calls). + Reading the first value sends all pending lazy operations in the same session to the server in a single request. + + + + +### How to apply a code fix + +Apply a fix through your IDE's quick actions, or run the following command: + +```shell +dotnet format analyzers MySolution.sln --diagnostics RVN011 RVN012 --severity info +``` +
+ +Replace `MySolution.sln` with the path to your project or solution file. + +The `--severity info` option includes diagnostics reported at `Info`. +By default, `dotnet format analyzers` fixes only diagnostics reported at `Warning` or `Error`. +For more options, see the [`dotnet format` documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format). + +
+ +
+ + + +| ID | Title | Area | Automated Fix | +|------------------------|----------------------------------------------------------------------|------------------|--------------------| +| [RVN001](./RVN001.mdx) | Index Map or Reduce assigned outside constructor | Index definition | No | +| [RVN002](./RVN002.mdx) | RavenDB query operator after projection | Query | No | +| [RVN003](./RVN003.mdx) | ProjectInto called more than once in a query chain | Query | No | +| [RVN004](./RVN004.mdx) | AbstractIndexCreationTask subclass is missing a Map assignment | Index definition | No | +| [RVN005](./RVN005.mdx) | Multi-map index has no AddMap call in any constructor | Index definition | No | +| [RVN006](./RVN006.mdx) | Multi-map index uses only a single AddMap | Index definition | No | +| [RVN007](./RVN007.mdx) | Query field not defined in the index | Query | No | +| [RVN008](./RVN008.mdx) | Projected field not retrievable under the applied ProjectionBehavior | Query | No | +| [RVN009](./RVN009.mdx) | Unsupported method call inside index Map/Reduce expression | Index definition | No | +| [RVN010](./RVN010.mdx) | Unsupported method call inside RavenDB query expression | Query | No | +| [RVN011](./RVN011.mdx) | Use batch.OpenSession inside a subscription Run delegate | Subscriptions | Yes | +| [RVN012](./RVN012.mdx) | Batch independent session operations using the lazy API | Session | Yes | +| [RVN013](./RVN013.mdx) | Query result is not bounded by Take() | Query | No | +| [RVN014](./RVN014.mdx) | Index Map fans out over a collection | Index definition | No | +| [RVN015](./RVN015.mdx) | Correlated conditions over a nested collection in a dynamic query | Query | No | + +