From 3731bfd5858b25c4e564ba800a5b094d2efe94ef Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Wed, 13 May 2026 10:28:41 +0200 Subject: [PATCH 1/5] RavenDB-26449: initial docs payload --- docs/analyzers/RVN001.md | 57 ++++++++++++++++++++++++++++ docs/analyzers/RVN002.md | 47 +++++++++++++++++++++++ docs/analyzers/RVN003.md | 42 +++++++++++++++++++++ docs/analyzers/RVN004.md | 48 ++++++++++++++++++++++++ docs/analyzers/RVN005.md | 48 ++++++++++++++++++++++++ docs/analyzers/RVN006.md | 50 +++++++++++++++++++++++++ docs/analyzers/RVN007.md | 58 +++++++++++++++++++++++++++++ docs/analyzers/RVN008.md | 65 ++++++++++++++++++++++++++++++++ docs/analyzers/RVN009.md | 53 ++++++++++++++++++++++++++ docs/analyzers/RVN010.md | 59 +++++++++++++++++++++++++++++ docs/analyzers/RVN011.md | 55 +++++++++++++++++++++++++++ docs/analyzers/RVN012.md | 72 ++++++++++++++++++++++++++++++++++++ docs/analyzers/RVN013.md | 51 +++++++++++++++++++++++++ docs/analyzers/RVN014.md | 76 ++++++++++++++++++++++++++++++++++++++ docs/analyzers/overview.md | 45 ++++++++++++++++++++++ 15 files changed, 826 insertions(+) create mode 100644 docs/analyzers/RVN001.md create mode 100644 docs/analyzers/RVN002.md create mode 100644 docs/analyzers/RVN003.md create mode 100644 docs/analyzers/RVN004.md create mode 100644 docs/analyzers/RVN005.md create mode 100644 docs/analyzers/RVN006.md create mode 100644 docs/analyzers/RVN007.md create mode 100644 docs/analyzers/RVN008.md create mode 100644 docs/analyzers/RVN009.md create mode 100644 docs/analyzers/RVN010.md create mode 100644 docs/analyzers/RVN011.md create mode 100644 docs/analyzers/RVN012.md create mode 100644 docs/analyzers/RVN013.md create mode 100644 docs/analyzers/RVN014.md create mode 100644 docs/analyzers/overview.md diff --git a/docs/analyzers/RVN001.md b/docs/analyzers/RVN001.md new file mode 100644 index 0000000000..82290f3813 --- /dev/null +++ b/docs/analyzers/RVN001.md @@ -0,0 +1,57 @@ +# RVN001 — Index Map or Reduce assigned outside constructor + +| Property | Value | +|----------|-------| +| **ID** | RVN001 | +| **Title** | Index Map or Reduce assigned outside constructor | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB reads the `Map` and `Reduce` expression trees during index registration, which happens when the index class constructor runs. Assigning `Map` or `Reduce` in a regular method, even one called from the constructor, means the assignment may never execute (e.g., when the method is only called conditionally), resulting in the index being deployed without a definition. + +## Trigger condition + +The analyzer fires when an assignment to `Map` or `Reduce` (the properties defined on `AbstractIndexCreationTask` and its variants) appears inside a regular `MethodDeclarationSyntax` rather than inside a constructor body. The check is semantic: it confirms the target identifier resolves to the actual `Map`/`Reduce` field or property on an index base class before reporting. + +## What to do + +Move the `Map` and `Reduce` for map-reduce indexes assignment into the constructor body. + +### Before (triggers RVN001) + +```csharp +public class OrdersByCompany : AbstractIndexCreationTask +{ + public void Configure() + { + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } + + public OrdersByCompany() + { + Configure(); // assignment happens in a method, not directly in ctor + } +} +``` + +### After (correct) + +```csharp +public class OrdersByCompany : AbstractIndexCreationTask +{ + public OrdersByCompany() + { + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } +} +``` + +## See also + +- [RVN004](RVN004.md) — fires when no constructor assigns `Map` at all. +- [RVN005](RVN005.md) — same concept for multi-map indexes missing `AddMap`. diff --git a/docs/analyzers/RVN002.md b/docs/analyzers/RVN002.md new file mode 100644 index 0000000000..9daa2fae17 --- /dev/null +++ b/docs/analyzers/RVN002.md @@ -0,0 +1,47 @@ +# RVN002 — RavenDB query operator after projection + +| Property | Value | +|----------|-------| +| **ID** | RVN002 | +| **Title** | RavenDB query operator after projection | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB LINQ query operators such as `Where`, `OrderBy`, `Search`, `Spatial`, `VectorSearch`, `Filter`, `GroupBy`, and their variants bind against the **source document or index shape**. When `.ProjectInto()` or `.Select(x => new { … })` appears earlier in the chain, it changes the element type. Any operator placed *after* the projection then operates on the projected type instead of the source type, silently producing incorrect results or throwing at runtime. + +## Operators covered + +The following methods are flagged when they appear after a projection: `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, `GroupBy`, `Search`, `Spatial`, `OrderByDistance`, `OrderByDistanceDescending`, `OrderByScore`, `OrderByScoreDescending`, `ThenByScore`, `ThenByScoreDescending`, `MoreLikeThis`, `VectorSearch`, `Filter`, `GroupByArrayValues`, `GroupByArrayContent`. + +## Trigger condition + +The analyzer fires when the immediate receiver of a forbidden method is an `IRavenQueryable`, and walking back through the invocation chain finds a `.ProjectInto` or `.Select` call whose own receiver is also an `IRavenQueryable`. + +## What to do + +Move the projection call to the **end** of the query chain, after all filtering and ordering. + +### Before (triggers RVN002) + +```csharp +var results = session.Query() + .ProjectInto() + .Where(x => x.Company == "companies/1-A") // RVN002: Where after ProjectInto + .ToList(); +``` + +### After (correct) + +```csharp +var results = session.Query() + .Where(x => x.Company == "companies/1-A") + .ProjectInto() + .ToList(); +``` + +## See also + +- [RVN003](RVN003.md) — fires when `.ProjectInto` is called twice on the same chain. diff --git a/docs/analyzers/RVN003.md b/docs/analyzers/RVN003.md new file mode 100644 index 0000000000..66192d2466 --- /dev/null +++ b/docs/analyzers/RVN003.md @@ -0,0 +1,42 @@ +# RVN003 — ProjectInto called more than once in a query chain + +| Property | Value | +|----------|-------| +| **ID** | RVN003 | +| **Title** | ProjectInto called more than once in a query chain | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB's `ProjectInto()` can only be called once per query chain. Calling it a second time throws `InvalidOperationException` at runtime because the projection is already registered on the query provider. This diagnostic catches the double-call at compile time. + +## Trigger condition + +The analyzer fires when a `.ProjectInto` invocation on an `IRavenQueryable` receiver is found, and walking back through the invocation chain finds a previous `.ProjectInto` call whose own receiver is also an `IRavenQueryable`. + +## What to do + +Remove the duplicate `.ProjectInto` call. If you need to project into a different type, restructure the query or use a single projection with a combined DTO. + +### Before (triggers RVN003) + +```csharp +var results = session.Query() + .ProjectInto() + .ProjectInto() // RVN003: second ProjectInto throws at runtime + .ToList(); +``` + +### After (correct) + +```csharp +var results = session.Query() + .ProjectInto() + .ToList(); +``` + +## See also + +- [RVN002](RVN002.md) — fires when a query operator appears after a projection. diff --git a/docs/analyzers/RVN004.md b/docs/analyzers/RVN004.md new file mode 100644 index 0000000000..f3b067af4f --- /dev/null +++ b/docs/analyzers/RVN004.md @@ -0,0 +1,48 @@ +# RVN004 — AbstractIndexCreationTask subclass is missing a Map assignment + +| Property | Value | +|----------|-------| +| **ID** | RVN004 | +| **Title** | AbstractIndexCreationTask subclass is missing a Map assignment | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +Every class that inherits from `AbstractIndexCreationTask` (or `AbstractGenericIndexCreationTask`) must assign the `Map` property in its constructor. Without a `Map` expression, the index has no definition and will fail when deployed to the server. + +## Trigger condition + +The analyzer fires when a class has a base-type chain that includes `AbstractIndexCreationTask` or `AbstractGenericIndexCreationTask`, and none of its constructors contains an assignment to the `Map` property that resolves to the field or property defined on the index base class. JavaScript indexes (`AbstractJavaScriptIndexCreationTask`) are excluded because they express their maps differently. + +## What to do + +Add a constructor (or update the existing one) that assigns the `Map` property. + +### Before (triggers RVN004) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + // No constructor — Map is never assigned; RVN004 fires on the class name +} +``` + +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders + select new { o.Company, o.Employee }; + } +} +``` + +## See also + +- [RVN001](RVN001.md) — fires when `Map` is assigned in a method rather than a constructor. +- [RVN005](RVN005.md) — equivalent rule for multi-map indexes. diff --git a/docs/analyzers/RVN005.md b/docs/analyzers/RVN005.md new file mode 100644 index 0000000000..f5836b1e79 --- /dev/null +++ b/docs/analyzers/RVN005.md @@ -0,0 +1,48 @@ +# RVN005 — Multi-map index has no AddMap call in any constructor + +| Property | Value | +|----------|-------| +| **ID** | RVN005 | +| **Title** | Multi-map index has no AddMap call in any constructor | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +Every class that inherits from `AbstractMultiMapIndexCreationTask` (or the time-series and counters variants: `AbstractMultiMapTimeSeriesIndexCreationTask`, `AbstractMultiMapCountersIndexCreationTask`) must call `AddMap(…)` at least once in its constructor. Without at least one `AddMap`, the index has no definition and will fail when deployed to the server. + +## Trigger condition + +The analyzer fires when a class whose base-type chain includes a multi-map index base class has no constructor that calls `AddMap` (or `AddMapForAll`) with the method resolving to the one defined on the multi-map base class. + +## What to do + +Add a constructor that calls `AddMap(…)` for each document type the index should map. + +### Before (triggers RVN005) + +```csharp +public class Animals_ByName : AbstractMultiMapIndexCreationTask +{ + // No AddMap call anywhere — RVN005 fires on the class name +} +``` + +### 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 }); + } +} +``` + +## See also + +- [RVN004](RVN004.md) — equivalent rule for regular single-map indexes. +- [RVN006](RVN006.md) — fires when a multi-map index has only one `AddMap` call. diff --git a/docs/analyzers/RVN006.md b/docs/analyzers/RVN006.md new file mode 100644 index 0000000000..90f78d0642 --- /dev/null +++ b/docs/analyzers/RVN006.md @@ -0,0 +1,50 @@ +# RVN006 — Multi-map index uses only a single AddMap + +| Property | Value | +|----------|-------| +| **ID** | RVN006 | +| **Title** | Multi-map index uses only a single AddMap | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +Multi-map index base classes (`AbstractMultiMapIndexCreationTask` and its variants) are designed for indexes that map over multiple document types. When only one `AddMap` call is present across all constructors, a regular `AbstractIndexCreationTask` is simpler, equally expressive, and has slightly lower overhead. + +## Trigger condition + +The analyzer fires when the class inherits from a multi-map index base class, and the total count of `AddMap` / `AddMapForAll` calls across all constructors is exactly one. + +## What to do + +Replace the multi-map base class with `AbstractIndexCreationTask` and replace the `AddMap(lambda)` call with a `Map = lambda` assignment. + +### Before (triggers RVN006) + +```csharp +public class Orders_ByCompany : AbstractMultiMapIndexCreationTask +{ + public Orders_ByCompany() + { + AddMap(orders => from o in orders select new { o.Company }); + // Only one AddMap — a regular index would suffice + } +} +``` + +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders select new { o.Company }; + } +} +``` + +## See also + +- [RVN005](RVN005.md) — fires when a multi-map index has zero `AddMap` calls. diff --git a/docs/analyzers/RVN007.md b/docs/analyzers/RVN007.md new file mode 100644 index 0000000000..a753336fb3 --- /dev/null +++ b/docs/analyzers/RVN007.md @@ -0,0 +1,58 @@ +# RVN007 — Query field not present in the index projection + +| Property | Value | +|----------|-------| +| **ID** | RVN007 | +| **Title** | Query field not present in the index projection | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +When querying a specific index (via `session.Query()`), all fields referenced in `Where`, `OrderBy`, and `Search` lambdas should match the fields projected by the index's `Map` expression. A field that is not part of the index projection cannot be searched or sorted by the server, and the query will return no results for that field. + +## Trigger condition + +The analyzer fires when a `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, or `Search` lambda is found on an `IRavenQueryable`, the query was created with an explicit index type argument (e.g., `Query()`), and a member referenced in the lambda does not appear in the set of fields extracted from the index's `Map` expression. The index class must be available in the same compilation for the field extraction to succeed; if the index is in a separate assembly the check is skipped conservatively. + +## What to do + +Ensure the fields you query on are part of the index projection, or switch to a different index that includes those fields. + +### Before (triggers RVN007) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders select new { o.Company }; + // 'Employee' is not in the projection + } +} + +var results = session.Query() + .Where(x => x.Employee == "employees/1-A") // RVN007: Employee not indexed + .ToList(); +``` + +### After (correct) + +```csharp +public class Orders_ByCompany : AbstractIndexCreationTask +{ + public Orders_ByCompany() + { + Map = orders => from o in orders select new { o.Company, o.Employee }; + } +} + +var results = session.Query() + .Where(x => x.Employee == "employees/1-A") + .ToList(); +``` + +## See also + +- [RVN008](RVN008.md) — fires when a projected field is not retrievable from the index or source document. diff --git a/docs/analyzers/RVN008.md b/docs/analyzers/RVN008.md new file mode 100644 index 0000000000..74f027eed2 --- /dev/null +++ b/docs/analyzers/RVN008.md @@ -0,0 +1,65 @@ +# RVN008 — Projected field not retrievable under the applied ProjectionBehavior + +| Property | Value | +|----------|-------| +| **ID** | RVN008 | +| **Title** | Projected field not retrievable under the applied ProjectionBehavior | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB projections retrieve each field from the stored index entry first, then fall back to the source document if the field is not stored. If a field is absent from **both** the stored index entry and the source document type, the projected value will be `null` or missing at runtime. Under `ProjectionBehavior.FromIndex` or `ProjectionBehavior.FromIndexOrThrow` the fallback to the source document is disabled, making unstored fields silently empty or a hard error respectively. + +## Analyzed projection forms + +- `ProjectInto()` — all public readable members of `TDto` are checked. +- `Select(x => new { x.Field1, x.Field2 })` — anonymous object member initializers. +- `Select(x => new MyDto { Field1 = x.Field1 })` — named object member initializers. + +Constructor-call projections (`new MyDto(x.Field1, x.Field2)`) and identity selects (`Select(x => x.SubProperty)`) are **not** analyzed. + +## Trigger condition + +The analyzer fires when a projection is used on an `IRavenQueryable` with an explicit index type, a projected member is neither stored in the index nor a member of the source document type, and the applicable `ProjectionBehavior` would not silently fall through (or the behavior is `Default` and the field would simply be `null`). + +## What to do + +Either store the field in the index (`Store(x => x.MyField, FieldStorage.Yes)`) or ensure the field exists on the source document type, or remove the field from the projection. + +### Before (triggers RVN008) + +```csharp +public class Orders_WithTotal : AbstractIndexCreationTask +{ + public Orders_WithTotal() + { + Map = orders => from o in orders select new { o.Company }; + // 'Total' is not a property of Order and is not stored in the index + } +} + +// OrderDto has Company and Total; Total won't be filled +var results = session.Query() + .ProjectInto() + .ToList(); +``` + +### After (store the computed field) + +```csharp +public class Orders_WithTotal : AbstractIndexCreationTask +{ + public Orders_WithTotal() + { + Map = orders => from o in orders + select new { o.Company, Total = o.Lines.Sum(l => l.PricePerUnit) }; + Store(x => x.Total, FieldStorage.Yes); + } +} +``` + +## See also + +- [RVN007](RVN007.md) — fires when a query filter field is not in the index projection. diff --git a/docs/analyzers/RVN009.md b/docs/analyzers/RVN009.md new file mode 100644 index 0000000000..89b47674ef --- /dev/null +++ b/docs/analyzers/RVN009.md @@ -0,0 +1,53 @@ +# RVN009 — Unsupported method call inside index Map/Reduce expression + +| Property | Value | +|----------|-------| +| **ID** | RVN009 | +| **Title** | Unsupported method call inside index Map/Reduce expression | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB compiles index `Map` and `Reduce` expressions to server-side IL using its own expression compiler. User-defined helper methods (methods declared in your own code) cannot be translated by this compiler and will cause the index to fail at deployment or produce incorrect results. Only constructs that RavenDB understands natively are allowed: standard LINQ operators, BCL string and math operations, `LoadDocument`, `CreateField`, `CreateSpatialField`, and a small set of RavenDB-specific functions. + +## Trigger condition + +The analyzer fires when an invocation inside a `Map`, `Reduce`, or `AddMap` lambda resolves to a method that is defined in user code (not in a framework assembly), is not an extension method (extension methods are excluded to reduce false positives), and is not a known-translatable BCL or RavenDB method. JavaScript indexes (`AbstractJavaScriptIndexCreationTask`) are excluded entirely. + +## What to do + +Inline the helper method's logic directly in the lambda, or replace it with supported constructs. + +### Before (triggers RVN009) + +```csharp +private static string Normalize(string s) => s.Trim().ToLower(); + +public class Products_ByName : AbstractIndexCreationTask +{ + public Products_ByName() + { + Map = products => from p in products + select new { Name = Normalize(p.Name) }; // RVN009 + } +} +``` + +### After (inline the logic) + +```csharp +public class Products_ByName : AbstractIndexCreationTask +{ + public Products_ByName() + { + Map = products => from p in products + select new { Name = p.Name.Trim().ToLower() }; + } +} +``` + +## See also + +- [RVN010](RVN010.md) — equivalent rule for user-defined methods inside query lambdas. diff --git a/docs/analyzers/RVN010.md b/docs/analyzers/RVN010.md new file mode 100644 index 0000000000..d7eaf58aaa --- /dev/null +++ b/docs/analyzers/RVN010.md @@ -0,0 +1,59 @@ +# RVN010 — Unsupported method call inside RavenDB query expression + +| Property | Value | +|----------|-------| +| **ID** | RVN010 | +| **Title** | Unsupported method call inside RavenDB query expression | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB translates LINQ query lambdas (`Where`, `OrderBy`, `Select`, `Filter`, `Spatial`, `VectorSearch`, `OrderByDistance`, `MoreLikeThis`, etc.) to RQL on the server. User-defined methods called inside these lambdas cannot be translated and will throw at runtime during RQL generation. Extension methods are excluded from this check to reduce false positives. + +## Query methods checked + +Lambdas inside the following query chain methods are analyzed: `Where`, `OrderBy`, `OrderByDescending`, `ThenBy`, `ThenByDescending`, `Select`, `SelectMany`, `GroupBy`, `Search`, `ProjectInto`, `Filter`, `Spatial`, `VectorSearch`, `OrderByDistance`, `OrderByDistanceDescending`, `MoreLikeThis`, `GroupByArrayValues`, `GroupByArrayContent`. + +## Trigger condition + +The analyzer fires when one of the above methods is called on an `IRavenQueryable`, and a lambda argument contains an invocation that resolves to a user-defined, non-extension method. + +## What to do + +1. **Compute the value before the query** — capture the result in a local variable and use it in the lambda. +2. **Inline the logic** — replace the method call with the equivalent LINQ/BCL expression that RavenDB can translate. +3. **Materialize client-side** — call `.ToList()` first and apply the filter/projection in memory using regular LINQ to Objects. + +### Before (triggers RVN010) + +```csharp +private static string GetCategory(string name) => name.Split('/')[0]; + +var results = session.Query() + .Where(x => GetCategory(x.Name) == "electronics") // RVN010 + .ToList(); +``` + +### After (option 1: compute before the query) + +```csharp +string category = "electronics"; +var results = session.Query() + .Where(x => x.Category == category) + .ToList(); +``` + +### After (option 3: materialize client-side) + +```csharp +var results = session.Query() + .ToList() + .Where(x => GetCategory(x.Name) == "electronics") + .ToList(); +``` + +## See also + +- [RVN009](RVN009.md) — equivalent rule for unsupported methods inside index `Map`/`Reduce` lambdas. diff --git a/docs/analyzers/RVN011.md b/docs/analyzers/RVN011.md new file mode 100644 index 0000000000..f7f65a8348 --- /dev/null +++ b/docs/analyzers/RVN011.md @@ -0,0 +1,55 @@ +# RVN011 — Use batch.OpenSession inside a subscription Run delegate + +| Property | Value | +|----------|-------| +| **ID** | RVN011 | +| **Title** | Use batch.OpenSession inside a subscription Run delegate | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | Yes | + +## Description + +Inside a subscription worker `Run` delegate the session must be opened via the **batch object** (`batch.OpenSession()` / `batch.OpenAsyncSession()`), not via the document store. The batch creates a session that participates in the batch's internal acknowledge transaction. When the session is opened from the document store directly, it bypasses that tracking — the batch will acknowledge successfully, but the session's changes are not tied to the acknowledgement, so documents may be re-processed if the worker restarts between the store commit and the batch ack. + +## Trigger condition + +The analyzer fires when `OpenSession` or `OpenAsyncSession` is called on a receiver whose type implements `IDocumentStore`, and the call appears inside a lambda (simple or parenthesized) that is passed as an argument to a `Run` method on a `SubscriptionWorker` or `AbstractSubscriptionWorker` instance. Named method-group references are not detected. + +## What to do + +Replace `store.OpenSession()` / `store.OpenAsyncSession()` with `batch.OpenSession()` / `batch.OpenAsyncSession()`, where `batch` is the parameter of the `Run` lambda. The automated code fix performs this replacement automatically. + +### Before (triggers RVN011) + +```csharp +worker.Run(async batch => +{ + using var session = store.OpenAsyncSession(); // RVN011 + foreach (var item in batch.Items) + { + var doc = await session.LoadAsync(item.Id); + // ... process doc + } + await session.SaveChangesAsync(); +}); +``` + +### After (correct / code fix result) + +```csharp +worker.Run(async batch => +{ + using var session = batch.OpenAsyncSession(); + foreach (var item in batch.Items) + { + var doc = await session.LoadAsync(item.Id); + // ... process doc + } + await session.SaveChangesAsync(); +}); +``` + +## Code fix details + +The fix replaces the store-typed receiver expression with the name of the first parameter of the enclosing `Run` lambda. It bails out when the call is inside a nested lambda (where the batch parameter is not directly in scope) to avoid producing incorrect code. diff --git a/docs/analyzers/RVN012.md b/docs/analyzers/RVN012.md new file mode 100644 index 0000000000..39fc5cd4f4 --- /dev/null +++ b/docs/analyzers/RVN012.md @@ -0,0 +1,72 @@ +# RVN012 — Batch independent session operations using the lazy API + +| Property | Value | +|----------|-------| +| **ID** | RVN012 | +| **Title** | Batch independent session operations using the lazy API | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | Yes | + +## Description + +Each eager `Load` or materializing query (`ToList`, `First`, `Single`, etc.) sends a separate HTTP request to the RavenDB server. When a method contains two or more independent operations on the same session, they can be registered as lazy and executed together in a single **multi-get** request, reducing network round-trips and latency. + +An operation is considered **independent** when its input (the document ID for `Load`, the query definition for a query) does not depend on the result of another operation in the same method. + +## Detected operations + +- `session.Load(id)` / `session.LoadAsync(id)` — when the `id` argument is a literal, a method parameter, a field, a property, or a local variable not derived from a prior load or query result. +- Materializing query calls (`ToList`, `ToListAsync`, `ToArray`, `ToArrayAsync`, `First`, `Single`, `Any`, `Count`, etc.) on an `IRavenQueryable`. + +## Trigger condition + +The analyzer fires when, inside a single method body (excluding nested lambdas and local functions), two or more of the above operations resolve to the same session symbol and are determined to be independent. + +## What to do + +Convert the eager calls to their lazy equivalents, then call `session.Advanced.Eagerly.ExecuteAllPendingLazyOperations()` (or the async variant) once to trigger the batch. The automated code fix performs the complete transformation when all flagged statements are consecutive and refer to the same session. + +### Before (triggers RVN012) + +```csharp +var order = session.Load("orders/1-A"); // RVN012 — round-trip 1 +var product = session.Load("products/1-A"); // RVN012 — round-trip 2 +``` + +### After (manual) + +```csharp +var lazyOrder = session.Advanced.Lazily.Load("orders/1-A"); +var lazyProduct = session.Advanced.Lazily.Load("products/1-A"); +session.Advanced.Eagerly.ExecuteAllPendingLazyOperations(); + +var order = lazyOrder.Value; +var product = lazyProduct.Value; +``` + +### Async variant + +```csharp +var lazyOrder = session.Advanced.Lazily.LoadAsync("orders/1-A"); +var lazyProduct = session.Advanced.Lazily.LoadAsync("products/1-A"); +await session.Advanced.Eagerly.ExecuteAllPendingLazyOperationsAsync(); + +var order = await lazyOrder.Value; +var product = await lazyProduct.Value; +``` + +### Mixing loads and queries + +```csharp +var lazyOrder = session.Advanced.Lazily.Load("orders/1-A"); +var lazySummary = session.Query().Where(x => x.Company == "companies/1").Lazily(); +session.Advanced.Eagerly.ExecuteAllPendingLazyOperations(); + +var order = lazyOrder.Value; +var summary = lazySummary.Value.ToList(); +``` + +## Code fix details + +The fix rewrites all flagged consecutive statements: replaces each `Load` with `session.Advanced.Lazily.Load[Async](id)`, replaces each materializing query call with `query.Lazily()`, inserts `ExecuteAllPendingLazyOperations[Async]()` after the last lazy registration, then replaces each original variable declaration with `.Value`. The fix bails out entirely if any statement cannot be safely rewritten, to avoid partial transformations. diff --git a/docs/analyzers/RVN013.md b/docs/analyzers/RVN013.md new file mode 100644 index 0000000000..a2674a92f0 --- /dev/null +++ b/docs/analyzers/RVN013.md @@ -0,0 +1,51 @@ +# RVN013 — Query result is not bounded by Take() + +| Property | Value | +|----------|-------| +| **ID** | RVN013 | +| **Title** | Query result is not bounded by Take() | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +RavenDB queries default to returning at most **128 documents** per request (the server's default page size). Without an explicit `.Take(n)`, this limit is invisible in the code, and the query may silently fetch far more data than intended as the dataset grows. Making the limit explicit with `.Take(n)` communicates intent clearly and prevents accidental full-scan materializations. + +## Trigger condition + +The analyzer fires when a materializing call (`ToList`, `ToListAsync`, `ToArray`, `ToArrayAsync`) is made on an `IRavenQueryable`, and walking back through the invocation chain finds no `.Take(…)` call. + +## What to do + +Add `.Take(n)` before the materializing call to make the result size explicit. + +### Before (triggers RVN013) + +```csharp +var orders = session.Query() + .Where(x => x.Status == OrderStatus.Open) + .ToList(); // RVN013 — no Take(); will return at most 128 results silently +``` + +### After (correct) + +```csharp +var orders = session.Query() + .Where(x => x.Status == OrderStatus.Open) + .Take(50) + .ToList(); +``` + +If fetching all results is intentional and the dataset is known to be small, use `int.MaxValue` to make that explicit: + +```csharp +var orders = session.Query() + .Where(x => x.Status == OrderStatus.Open) + .Take(int.MaxValue) + .ToList(); +``` + +## Note on single-result queries + +`First`, `FirstOrDefault`, `Single`, `SingleOrDefault`, and their async variants inherently return one document and are not flagged by this rule. diff --git a/docs/analyzers/RVN014.md b/docs/analyzers/RVN014.md new file mode 100644 index 0000000000..634b4c1370 --- /dev/null +++ b/docs/analyzers/RVN014.md @@ -0,0 +1,76 @@ +# RVN014 — Index Map fans out over a collection + +| Property | Value | +|----------|-------| +| **ID** | RVN014 | +| **Title** | Index Map fans out over a collection | +| **Severity** | Info (default) | +| **Category** | Usage | +| **Code fix** | No | + +## Description + +A *fan-out index* produces **multiple index entries per source document** by iterating over a nested collection. For example, an index that maps each order's line items produces one entry per line item instead of one per order. When collections are large or unbounded, this can significantly degrade indexing performance and storage. The RavenDB server independently fires a `WarnIndexOutputsPerDocument` warning at runtime for the same reason; this rule surfaces the issue at compile time before the index is deployed. + +**Fan-out is sometimes intentional.** The diagnostic is informational: verify that the fan-out is deliberate and that the collection's cardinality is acceptable for your workload. + +## Trigger condition + +The analyzer fires when a `Map` assignment or `AddMap`/`AddMapForAll` call inside an index constructor contains a `SelectMany(…)` invocation in method-chain form, or a nested `from` clause in query-expression form. The `Reduce` lambda is intentionally excluded — a Reduce runs over already-mapped entries and cannot produce additional outputs per source document. JavaScript indexes (`AbstractJavaScriptIndexCreationTask`) are excluded. + +## What to do + +If the fan-out is intentional, no action is required. Consider suppressing the diagnostic if the noise is unwelcome: + +```ini +[*.cs] +dotnet_diagnostic.RVN014.severity = none +``` + +If the fan-out is accidental, refactor the map to project a single entry per document (e.g., aggregate the nested collection instead of flattening it). + +### Fan-out — method-chain form (triggers RVN014) + +```csharp +public class Orders_ByLineItem : AbstractIndexCreationTask +{ + public Orders_ByLineItem() + { + Map = orders => orders.SelectMany( // RVN014 + o => o.Lines, + (o, line) => new { line.Product, line.Quantity }); + } +} +``` + +### Fan-out — query-expression form (triggers RVN014) + +```csharp +public class Orders_ByLineItem : AbstractIndexCreationTask +{ + public Orders_ByLineItem() + { + Map = orders => + from o in orders + from line in o.Lines // RVN014: nested from + select new { line.Product, line.Quantity }; + } +} +``` + +### Aggregated (no fan-out) + +```csharp +public class Orders_ByTotalQuantity : AbstractIndexCreationTask +{ + public Orders_ByTotalQuantity() + { + Map = orders => from o in orders + select new { TotalQuantity = o.Lines.Sum(l => l.Quantity) }; + } +} +``` + +## See also + +- Server-side `WarnIndexOutputsPerDocument` — the runtime counterpart to this compile-time rule. diff --git a/docs/analyzers/overview.md b/docs/analyzers/overview.md new file mode 100644 index 0000000000..49a5146a30 --- /dev/null +++ b/docs/analyzers/overview.md @@ -0,0 +1,45 @@ +# RavenDB Roslyn Analyzers + +RavenDB ships a set of Roslyn diagnostic analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. + +## Distribution + +Both analyzer assemblies (`Raven.Analyzers.dll` and `Raven.Analyzers.CodeFixes.dll`) are embedded in `RavenDB.Client.nupkg` under `analyzers/dotnet/cs/`. No separate install is required. The analyzers activate automatically on every build once the client package is referenced. + +## Severity + +All rules ship at **`DiagnosticSeverity.Info`** by default. This means they appear as informational hints in the IDE and in build output but **do not break existing builds on upgrade**. + +Teams can promote individual rules to `Warning` or `Error` via `.editorconfig` once existing occurrences have been addressed: + +```ini +[*.cs] +dotnet_diagnostic.RVN012.severity = warning +dotnet_diagnostic.RVN013.severity = error +``` + +## Code Fixes + +Two rules ship with automated code fixes (IDE lightbulb / `dotnet fix`): + +- **RVN011** — rewrites `store.OpenSession()` to `batch.OpenSession()` inside a subscription `Run` lambda. +- **RVN012** — rewrites a block of independent eager `Load` / `Query` calls into lazy registrations followed by a single `ExecuteAllPendingLazyOperations[Async]()` call. + +## Rules + +| ID | Title | Category | Code Fix | +|----|-------|----------|----------| +| [RVN001](RVN001.md) | Index Map or Reduce assigned outside constructor | Index definition | No | +| [RVN002](RVN002.md) | RavenDB query operator after projection | Query | No | +| [RVN003](RVN003.md) | ProjectInto called more than once in a query chain | Query | No | +| [RVN004](RVN004.md) | AbstractIndexCreationTask subclass is missing a Map assignment | Index definition | No | +| [RVN005](RVN005.md) | Multi-map index has no AddMap call in any constructor | Index definition | No | +| [RVN006](RVN006.md) | Multi-map index uses only a single AddMap | Index definition | No | +| [RVN007](RVN007.md) | Query field not present in the index projection | Query | No | +| [RVN008](RVN008.md) | Projected field not retrievable under the applied ProjectionBehavior | Query | No | +| [RVN009](RVN009.md) | Unsupported method call inside index Map/Reduce expression | Index definition | No | +| [RVN010](RVN010.md) | Unsupported method call inside RavenDB query expression | Query | No | +| [RVN011](RVN011.md) | Use batch.OpenSession inside a subscription Run delegate | Subscriptions | Yes | +| [RVN012](RVN012.md) | Batch independent session operations using the lazy API | Session | Yes | +| [RVN013](RVN013.md) | Query result is not bounded by Take() | Query | No | +| [RVN014](RVN014.md) | Index Map fans out over a collection | Index definition | No | From a52f93f8ea1c748b9fa99c7a5f1c5eea379c46e8 Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Wed, 13 May 2026 11:04:08 +0200 Subject: [PATCH 2/5] RavenDB-26449: docosaurusing it --- docs/analyzers/{RVN001.md => RVN001.mdx} | 10 ++- docs/analyzers/{RVN002.md => RVN002.mdx} | 8 ++- docs/analyzers/{RVN003.md => RVN003.mdx} | 8 ++- docs/analyzers/{RVN004.md => RVN004.mdx} | 10 ++- docs/analyzers/{RVN005.md => RVN005.mdx} | 10 ++- docs/analyzers/{RVN006.md => RVN006.mdx} | 8 ++- docs/analyzers/{RVN007.md => RVN007.mdx} | 8 ++- docs/analyzers/{RVN008.md => RVN008.mdx} | 8 ++- docs/analyzers/{RVN009.md => RVN009.mdx} | 8 ++- docs/analyzers/{RVN010.md => RVN010.mdx} | 8 ++- docs/analyzers/{RVN011.md => RVN011.mdx} | 6 ++ docs/analyzers/{RVN012.md => RVN012.mdx} | 6 ++ docs/analyzers/{RVN013.md => RVN013.mdx} | 6 ++ docs/analyzers/{RVN014.md => RVN014.mdx} | 6 ++ docs/analyzers/_category_.json | 1 + docs/analyzers/overview.md | 45 ------------- docs/analyzers/overview.mdx | 80 ++++++++++++++++++++++++ sidebars.ts | 5 ++ 18 files changed, 183 insertions(+), 58 deletions(-) rename docs/analyzers/{RVN001.md => RVN001.mdx} (86%) rename docs/analyzers/{RVN002.md => RVN002.mdx} (90%) rename docs/analyzers/{RVN003.md => RVN003.mdx} (86%) rename docs/analyzers/{RVN004.md => RVN004.mdx} (83%) rename docs/analyzers/{RVN005.md => RVN005.mdx} (83%) rename docs/analyzers/{RVN006.md => RVN006.mdx} (88%) rename docs/analyzers/{RVN007.md => RVN007.mdx} (90%) rename docs/analyzers/{RVN008.md => RVN008.mdx} (91%) rename docs/analyzers/{RVN009.md => RVN009.mdx} (89%) rename docs/analyzers/{RVN010.md => RVN010.mdx} (90%) rename docs/analyzers/{RVN011.md => RVN011.mdx} (94%) rename docs/analyzers/{RVN012.md => RVN012.mdx} (96%) rename docs/analyzers/{RVN013.md => RVN013.mdx} (94%) rename docs/analyzers/{RVN014.md => RVN014.mdx} (96%) create mode 100644 docs/analyzers/_category_.json delete mode 100644 docs/analyzers/overview.md create mode 100644 docs/analyzers/overview.mdx diff --git a/docs/analyzers/RVN001.md b/docs/analyzers/RVN001.mdx similarity index 86% rename from docs/analyzers/RVN001.md rename to docs/analyzers/RVN001.mdx index 82290f3813..a54d18c2e3 100644 --- a/docs/analyzers/RVN001.md +++ b/docs/analyzers/RVN001.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN001 — Index Map or Reduce assigned outside constructor" +sidebar_label: RVN001 +sidebar_position: 1 +--- + # RVN001 — Index Map or Reduce assigned outside constructor | Property | Value | @@ -53,5 +59,5 @@ public class OrdersByCompany : AbstractIndexCreationTask ## See also -- [RVN004](RVN004.md) — fires when no constructor assigns `Map` at all. -- [RVN005](RVN005.md) — same concept for multi-map indexes missing `AddMap`. +- [RVN004](./RVN004.mdx) — fires when no constructor assigns `Map` at all. +- [RVN005](./RVN005.mdx) — same concept for multi-map indexes missing `AddMap`. diff --git a/docs/analyzers/RVN002.md b/docs/analyzers/RVN002.mdx similarity index 90% rename from docs/analyzers/RVN002.md rename to docs/analyzers/RVN002.mdx index 9daa2fae17..76c4e243bb 100644 --- a/docs/analyzers/RVN002.md +++ b/docs/analyzers/RVN002.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN002 — RavenDB query operator after projection" +sidebar_label: RVN002 +sidebar_position: 2 +--- + # RVN002 — RavenDB query operator after projection | Property | Value | @@ -44,4 +50,4 @@ var results = session.Query() ## See also -- [RVN003](RVN003.md) — fires when `.ProjectInto` is called twice on the same chain. +- [RVN003](./RVN003.mdx) — fires when `.ProjectInto` is called twice on the same chain. diff --git a/docs/analyzers/RVN003.md b/docs/analyzers/RVN003.mdx similarity index 86% rename from docs/analyzers/RVN003.md rename to docs/analyzers/RVN003.mdx index 66192d2466..5796d4e047 100644 --- a/docs/analyzers/RVN003.md +++ b/docs/analyzers/RVN003.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN003 — ProjectInto called more than once in a query chain" +sidebar_label: RVN003 +sidebar_position: 3 +--- + # RVN003 — ProjectInto called more than once in a query chain | Property | Value | @@ -39,4 +45,4 @@ var results = session.Query() ## See also -- [RVN002](RVN002.md) — fires when a query operator appears after a projection. +- [RVN002](./RVN002.mdx) — fires when a query operator appears after a projection. diff --git a/docs/analyzers/RVN004.md b/docs/analyzers/RVN004.mdx similarity index 83% rename from docs/analyzers/RVN004.md rename to docs/analyzers/RVN004.mdx index f3b067af4f..95d51666a0 100644 --- a/docs/analyzers/RVN004.md +++ b/docs/analyzers/RVN004.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN004 — AbstractIndexCreationTask subclass is missing a Map assignment" +sidebar_label: RVN004 +sidebar_position: 4 +--- + # RVN004 — AbstractIndexCreationTask subclass is missing a Map assignment | Property | Value | @@ -44,5 +50,5 @@ public class Orders_ByCompany : AbstractIndexCreationTask ## See also -- [RVN001](RVN001.md) — fires when `Map` is assigned in a method rather than a constructor. -- [RVN005](RVN005.md) — equivalent rule for multi-map indexes. +- [RVN001](./RVN001.mdx) — fires when `Map` is assigned in a method rather than a constructor. +- [RVN005](./RVN005.mdx) — equivalent rule for multi-map indexes. diff --git a/docs/analyzers/RVN005.md b/docs/analyzers/RVN005.mdx similarity index 83% rename from docs/analyzers/RVN005.md rename to docs/analyzers/RVN005.mdx index f5836b1e79..e20d95429d 100644 --- a/docs/analyzers/RVN005.md +++ b/docs/analyzers/RVN005.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN005 — Multi-map index has no AddMap call in any constructor" +sidebar_label: RVN005 +sidebar_position: 5 +--- + # RVN005 — Multi-map index has no AddMap call in any constructor | Property | Value | @@ -44,5 +50,5 @@ public class Animals_ByName : AbstractMultiMapIndexCreationTask ## See also -- [RVN004](RVN004.md) — equivalent rule for regular single-map indexes. -- [RVN006](RVN006.md) — fires when a multi-map index has only one `AddMap` call. +- [RVN004](./RVN004.mdx) — equivalent rule for regular single-map indexes. +- [RVN006](./RVN006.mdx) — fires when a multi-map index has only one `AddMap` call. diff --git a/docs/analyzers/RVN006.md b/docs/analyzers/RVN006.mdx similarity index 88% rename from docs/analyzers/RVN006.md rename to docs/analyzers/RVN006.mdx index 90f78d0642..30f25b4dce 100644 --- a/docs/analyzers/RVN006.md +++ b/docs/analyzers/RVN006.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN006 — Multi-map index uses only a single AddMap" +sidebar_label: RVN006 +sidebar_position: 6 +--- + # RVN006 — Multi-map index uses only a single AddMap | Property | Value | @@ -47,4 +53,4 @@ public class Orders_ByCompany : AbstractIndexCreationTask ## See also -- [RVN005](RVN005.md) — fires when a multi-map index has zero `AddMap` calls. +- [RVN005](./RVN005.mdx) — fires when a multi-map index has zero `AddMap` calls. diff --git a/docs/analyzers/RVN007.md b/docs/analyzers/RVN007.mdx similarity index 90% rename from docs/analyzers/RVN007.md rename to docs/analyzers/RVN007.mdx index a753336fb3..2dfcee9bc3 100644 --- a/docs/analyzers/RVN007.md +++ b/docs/analyzers/RVN007.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN007 — Query field not present in the index projection" +sidebar_label: RVN007 +sidebar_position: 7 +--- + # RVN007 — Query field not present in the index projection | Property | Value | @@ -55,4 +61,4 @@ var results = session.Query() ## See also -- [RVN008](RVN008.md) — fires when a projected field is not retrievable from the index or source document. +- [RVN008](./RVN008.mdx) — fires when a projected field is not retrievable from the index or source document. diff --git a/docs/analyzers/RVN008.md b/docs/analyzers/RVN008.mdx similarity index 91% rename from docs/analyzers/RVN008.md rename to docs/analyzers/RVN008.mdx index 74f027eed2..07a27f6a4c 100644 --- a/docs/analyzers/RVN008.md +++ b/docs/analyzers/RVN008.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN008 — Projected field not retrievable under the applied ProjectionBehavior" +sidebar_label: RVN008 +sidebar_position: 8 +--- + # RVN008 — Projected field not retrievable under the applied ProjectionBehavior | Property | Value | @@ -62,4 +68,4 @@ public class Orders_WithTotal : AbstractIndexCreationTask ## See also -- [RVN007](RVN007.md) — fires when a query filter field is not in the index projection. +- [RVN007](./RVN007.mdx) — fires when a query filter field is not in the index projection. diff --git a/docs/analyzers/RVN009.md b/docs/analyzers/RVN009.mdx similarity index 89% rename from docs/analyzers/RVN009.md rename to docs/analyzers/RVN009.mdx index 89b47674ef..39c72860f9 100644 --- a/docs/analyzers/RVN009.md +++ b/docs/analyzers/RVN009.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN009 — Unsupported method call inside index Map/Reduce expression" +sidebar_label: RVN009 +sidebar_position: 9 +--- + # RVN009 — Unsupported method call inside index Map/Reduce expression | Property | Value | @@ -50,4 +56,4 @@ public class Products_ByName : AbstractIndexCreationTask ## See also -- [RVN010](RVN010.md) — equivalent rule for user-defined methods inside query lambdas. +- [RVN010](./RVN010.mdx) — equivalent rule for user-defined methods inside query lambdas. diff --git a/docs/analyzers/RVN010.md b/docs/analyzers/RVN010.mdx similarity index 90% rename from docs/analyzers/RVN010.md rename to docs/analyzers/RVN010.mdx index d7eaf58aaa..c96759dc8b 100644 --- a/docs/analyzers/RVN010.md +++ b/docs/analyzers/RVN010.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN010 — Unsupported method call inside RavenDB query expression" +sidebar_label: RVN010 +sidebar_position: 10 +--- + # RVN010 — Unsupported method call inside RavenDB query expression | Property | Value | @@ -56,4 +62,4 @@ var results = session.Query() ## See also -- [RVN009](RVN009.md) — equivalent rule for unsupported methods inside index `Map`/`Reduce` lambdas. +- [RVN009](./RVN009.mdx) — equivalent rule for unsupported methods inside index `Map`/`Reduce` lambdas. diff --git a/docs/analyzers/RVN011.md b/docs/analyzers/RVN011.mdx similarity index 94% rename from docs/analyzers/RVN011.md rename to docs/analyzers/RVN011.mdx index f7f65a8348..cafe78b880 100644 --- a/docs/analyzers/RVN011.md +++ b/docs/analyzers/RVN011.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN011 — Use batch.OpenSession inside a subscription Run delegate" +sidebar_label: RVN011 +sidebar_position: 11 +--- + # RVN011 — Use batch.OpenSession inside a subscription Run delegate | Property | Value | diff --git a/docs/analyzers/RVN012.md b/docs/analyzers/RVN012.mdx similarity index 96% rename from docs/analyzers/RVN012.md rename to docs/analyzers/RVN012.mdx index 39fc5cd4f4..c55f19e1b5 100644 --- a/docs/analyzers/RVN012.md +++ b/docs/analyzers/RVN012.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN012 — Batch independent session operations using the lazy API" +sidebar_label: RVN012 +sidebar_position: 12 +--- + # RVN012 — Batch independent session operations using the lazy API | Property | Value | diff --git a/docs/analyzers/RVN013.md b/docs/analyzers/RVN013.mdx similarity index 94% rename from docs/analyzers/RVN013.md rename to docs/analyzers/RVN013.mdx index a2674a92f0..d4336536fc 100644 --- a/docs/analyzers/RVN013.md +++ b/docs/analyzers/RVN013.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN013 — Query result is not bounded by Take()" +sidebar_label: RVN013 +sidebar_position: 13 +--- + # RVN013 — Query result is not bounded by Take() | Property | Value | diff --git a/docs/analyzers/RVN014.md b/docs/analyzers/RVN014.mdx similarity index 96% rename from docs/analyzers/RVN014.md rename to docs/analyzers/RVN014.mdx index 634b4c1370..bbbb032fab 100644 --- a/docs/analyzers/RVN014.md +++ b/docs/analyzers/RVN014.mdx @@ -1,3 +1,9 @@ +--- +title: "RVN014 — Index Map fans out over a collection" +sidebar_label: RVN014 +sidebar_position: 14 +--- + # RVN014 — Index Map fans out over a collection | Property | Value | diff --git a/docs/analyzers/_category_.json b/docs/analyzers/_category_.json new file mode 100644 index 0000000000..7c7c421484 --- /dev/null +++ b/docs/analyzers/_category_.json @@ -0,0 +1 @@ +{ "position": 14, "label": "Analyzers" } diff --git a/docs/analyzers/overview.md b/docs/analyzers/overview.md deleted file mode 100644 index 49a5146a30..0000000000 --- a/docs/analyzers/overview.md +++ /dev/null @@ -1,45 +0,0 @@ -# RavenDB Roslyn Analyzers - -RavenDB ships a set of Roslyn diagnostic analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. - -## Distribution - -Both analyzer assemblies (`Raven.Analyzers.dll` and `Raven.Analyzers.CodeFixes.dll`) are embedded in `RavenDB.Client.nupkg` under `analyzers/dotnet/cs/`. No separate install is required. The analyzers activate automatically on every build once the client package is referenced. - -## Severity - -All rules ship at **`DiagnosticSeverity.Info`** by default. This means they appear as informational hints in the IDE and in build output but **do not break existing builds on upgrade**. - -Teams can promote individual rules to `Warning` or `Error` via `.editorconfig` once existing occurrences have been addressed: - -```ini -[*.cs] -dotnet_diagnostic.RVN012.severity = warning -dotnet_diagnostic.RVN013.severity = error -``` - -## Code Fixes - -Two rules ship with automated code fixes (IDE lightbulb / `dotnet fix`): - -- **RVN011** — rewrites `store.OpenSession()` to `batch.OpenSession()` inside a subscription `Run` lambda. -- **RVN012** — rewrites a block of independent eager `Load` / `Query` calls into lazy registrations followed by a single `ExecuteAllPendingLazyOperations[Async]()` call. - -## Rules - -| ID | Title | Category | Code Fix | -|----|-------|----------|----------| -| [RVN001](RVN001.md) | Index Map or Reduce assigned outside constructor | Index definition | No | -| [RVN002](RVN002.md) | RavenDB query operator after projection | Query | No | -| [RVN003](RVN003.md) | ProjectInto called more than once in a query chain | Query | No | -| [RVN004](RVN004.md) | AbstractIndexCreationTask subclass is missing a Map assignment | Index definition | No | -| [RVN005](RVN005.md) | Multi-map index has no AddMap call in any constructor | Index definition | No | -| [RVN006](RVN006.md) | Multi-map index uses only a single AddMap | Index definition | No | -| [RVN007](RVN007.md) | Query field not present in the index projection | Query | No | -| [RVN008](RVN008.md) | Projected field not retrievable under the applied ProjectionBehavior | Query | No | -| [RVN009](RVN009.md) | Unsupported method call inside index Map/Reduce expression | Index definition | No | -| [RVN010](RVN010.md) | Unsupported method call inside RavenDB query expression | Query | No | -| [RVN011](RVN011.md) | Use batch.OpenSession inside a subscription Run delegate | Subscriptions | Yes | -| [RVN012](RVN012.md) | Batch independent session operations using the lazy API | Session | Yes | -| [RVN013](RVN013.md) | Query result is not bounded by Take() | Query | No | -| [RVN014](RVN014.md) | Index Map fans out over a collection | Index definition | No | diff --git a/docs/analyzers/overview.mdx b/docs/analyzers/overview.mdx new file mode 100644 index 0000000000..f755835ae2 --- /dev/null +++ b/docs/analyzers/overview.mdx @@ -0,0 +1,80 @@ +--- +title: "RavenDB Roslyn Analyzers" +sidebar_label: Overview +sidebar_position: 0 +see_also: + - title: "Creating and Deploying Indexes" + link: "indexes/creating-and-deploying" + source: "docs" + path: "Indexes" + - title: "Query Overview" + link: "querying/overview" + source: "docs" + path: "Querying" + - title: "Document Query" + link: "client-api/session/querying/document-query/what-is-document-query" + source: "docs" + path: "Client API > Session > 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'; + +# RavenDB Roslyn Analyzers + +RavenDB ships a set of Roslyn diagnostic analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. + +## Distribution + +Both analyzer assemblies (`Raven.Analyzers.dll` and `Raven.Analyzers.CodeFixes.dll`) are embedded in `RavenDB.Client.nupkg` under `analyzers/dotnet/cs/`. No separate install is required. The analyzers activate automatically on every build once the client package is referenced. + +## Severity + + + +All rules ship at **`DiagnosticSeverity.Info`** by default. They appear as informational hints in the IDE and in build output but **do not break existing builds on upgrade**. + +Teams can promote individual rules to `Warning` or `Error` via `.editorconfig` once existing occurrences have been addressed: + +```ini +[*.cs] +dotnet_diagnostic.RVN012.severity = warning +dotnet_diagnostic.RVN013.severity = error +``` + + + +## Code Fixes + + + +- **[RVN011](./RVN011.mdx)** — rewrites `store.OpenSession()` to `batch.OpenSession()` inside a subscription `Run` lambda. +- **[RVN012](./RVN012.mdx)** — rewrites a block of independent eager `Load` / `Query` calls into lazy registrations followed by a single `ExecuteAllPendingLazyOperations[Async]()` call. + + + +## Rules + +| ID | Title | Category | Code 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 present in the index projection | 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 | diff --git a/sidebars.ts b/sidebars.ts index ec614571af..f355e8c344 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -62,6 +62,11 @@ const sidebars: SidebarsConfig = { label: "Indexes", items: [{ type: "autogenerated", dirName: "indexes" }], }, + { + type: "category", + label: "Analyzers", + items: [{ type: "autogenerated", dirName: "analyzers" }], + }, { type: "category", label: "Compare-Exchange", From 86933fbc5b945afdc76524cccc959c311bbee293 Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Wed, 13 May 2026 11:25:39 +0200 Subject: [PATCH 3/5] RavenDB-26449: moving to a new route --- docs/analyzers/_category_.json | 1 - .../csharp-code-analyzers}/RVN001.mdx | 0 .../csharp-code-analyzers}/RVN002.mdx | 0 .../csharp-code-analyzers}/RVN003.mdx | 0 .../csharp-code-analyzers}/RVN004.mdx | 0 .../csharp-code-analyzers}/RVN005.mdx | 0 .../csharp-code-analyzers}/RVN006.mdx | 0 .../csharp-code-analyzers}/RVN007.mdx | 0 .../csharp-code-analyzers}/RVN008.mdx | 0 .../csharp-code-analyzers}/RVN009.mdx | 0 .../csharp-code-analyzers}/RVN010.mdx | 0 .../csharp-code-analyzers}/RVN011.mdx | 0 .../csharp-code-analyzers}/RVN012.mdx | 0 .../csharp-code-analyzers}/RVN013.mdx | 0 .../csharp-code-analyzers}/RVN014.mdx | 0 docs/client-api/csharp-code-analyzers/_category_.json | 1 + .../csharp-code-analyzers}/overview.mdx | 6 +++--- sidebars.ts | 5 ----- 18 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 docs/analyzers/_category_.json rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN001.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN002.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN003.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN004.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN005.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN006.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN007.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN008.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN009.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN010.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN011.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN012.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN013.mdx (100%) rename docs/{analyzers => client-api/csharp-code-analyzers}/RVN014.mdx (100%) create mode 100644 docs/client-api/csharp-code-analyzers/_category_.json rename docs/{analyzers => client-api/csharp-code-analyzers}/overview.mdx (92%) diff --git a/docs/analyzers/_category_.json b/docs/analyzers/_category_.json deleted file mode 100644 index 7c7c421484..0000000000 --- a/docs/analyzers/_category_.json +++ /dev/null @@ -1 +0,0 @@ -{ "position": 14, "label": "Analyzers" } diff --git a/docs/analyzers/RVN001.mdx b/docs/client-api/csharp-code-analyzers/RVN001.mdx similarity index 100% rename from docs/analyzers/RVN001.mdx rename to docs/client-api/csharp-code-analyzers/RVN001.mdx diff --git a/docs/analyzers/RVN002.mdx b/docs/client-api/csharp-code-analyzers/RVN002.mdx similarity index 100% rename from docs/analyzers/RVN002.mdx rename to docs/client-api/csharp-code-analyzers/RVN002.mdx diff --git a/docs/analyzers/RVN003.mdx b/docs/client-api/csharp-code-analyzers/RVN003.mdx similarity index 100% rename from docs/analyzers/RVN003.mdx rename to docs/client-api/csharp-code-analyzers/RVN003.mdx diff --git a/docs/analyzers/RVN004.mdx b/docs/client-api/csharp-code-analyzers/RVN004.mdx similarity index 100% rename from docs/analyzers/RVN004.mdx rename to docs/client-api/csharp-code-analyzers/RVN004.mdx diff --git a/docs/analyzers/RVN005.mdx b/docs/client-api/csharp-code-analyzers/RVN005.mdx similarity index 100% rename from docs/analyzers/RVN005.mdx rename to docs/client-api/csharp-code-analyzers/RVN005.mdx diff --git a/docs/analyzers/RVN006.mdx b/docs/client-api/csharp-code-analyzers/RVN006.mdx similarity index 100% rename from docs/analyzers/RVN006.mdx rename to docs/client-api/csharp-code-analyzers/RVN006.mdx diff --git a/docs/analyzers/RVN007.mdx b/docs/client-api/csharp-code-analyzers/RVN007.mdx similarity index 100% rename from docs/analyzers/RVN007.mdx rename to docs/client-api/csharp-code-analyzers/RVN007.mdx diff --git a/docs/analyzers/RVN008.mdx b/docs/client-api/csharp-code-analyzers/RVN008.mdx similarity index 100% rename from docs/analyzers/RVN008.mdx rename to docs/client-api/csharp-code-analyzers/RVN008.mdx diff --git a/docs/analyzers/RVN009.mdx b/docs/client-api/csharp-code-analyzers/RVN009.mdx similarity index 100% rename from docs/analyzers/RVN009.mdx rename to docs/client-api/csharp-code-analyzers/RVN009.mdx diff --git a/docs/analyzers/RVN010.mdx b/docs/client-api/csharp-code-analyzers/RVN010.mdx similarity index 100% rename from docs/analyzers/RVN010.mdx rename to docs/client-api/csharp-code-analyzers/RVN010.mdx diff --git a/docs/analyzers/RVN011.mdx b/docs/client-api/csharp-code-analyzers/RVN011.mdx similarity index 100% rename from docs/analyzers/RVN011.mdx rename to docs/client-api/csharp-code-analyzers/RVN011.mdx diff --git a/docs/analyzers/RVN012.mdx b/docs/client-api/csharp-code-analyzers/RVN012.mdx similarity index 100% rename from docs/analyzers/RVN012.mdx rename to docs/client-api/csharp-code-analyzers/RVN012.mdx diff --git a/docs/analyzers/RVN013.mdx b/docs/client-api/csharp-code-analyzers/RVN013.mdx similarity index 100% rename from docs/analyzers/RVN013.mdx rename to docs/client-api/csharp-code-analyzers/RVN013.mdx diff --git a/docs/analyzers/RVN014.mdx b/docs/client-api/csharp-code-analyzers/RVN014.mdx similarity index 100% rename from docs/analyzers/RVN014.mdx rename to docs/client-api/csharp-code-analyzers/RVN014.mdx 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/analyzers/overview.mdx b/docs/client-api/csharp-code-analyzers/overview.mdx similarity index 92% rename from docs/analyzers/overview.mdx rename to docs/client-api/csharp-code-analyzers/overview.mdx index f755835ae2..2f582c76f5 100644 --- a/docs/analyzers/overview.mdx +++ b/docs/client-api/csharp-code-analyzers/overview.mdx @@ -1,5 +1,5 @@ --- -title: "RavenDB Roslyn Analyzers" +title: "C# Code Analyzers" sidebar_label: Overview sidebar_position: 0 see_also: @@ -27,9 +27,9 @@ see_also: import Admonition from '@theme/Admonition'; -# RavenDB Roslyn Analyzers +# C# Code Analyzers -RavenDB ships a set of Roslyn diagnostic analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. +RavenDB ships a set of C# code analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. ## Distribution diff --git a/sidebars.ts b/sidebars.ts index f355e8c344..ec614571af 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -62,11 +62,6 @@ const sidebars: SidebarsConfig = { label: "Indexes", items: [{ type: "autogenerated", dirName: "indexes" }], }, - { - type: "category", - label: "Analyzers", - items: [{ type: "autogenerated", dirName: "analyzers" }], - }, { type: "category", label: "Compare-Exchange", From df4c5d47660b71e704aeae065b9b07da862bc044 Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Wed, 13 May 2026 11:27:28 +0200 Subject: [PATCH 4/5] RavenDB-26449: add a note aboute separate from the indexing analyzers --- docs/client-api/csharp-code-analyzers/overview.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/client-api/csharp-code-analyzers/overview.mdx b/docs/client-api/csharp-code-analyzers/overview.mdx index 2f582c76f5..3203ab27d1 100644 --- a/docs/client-api/csharp-code-analyzers/overview.mdx +++ b/docs/client-api/csharp-code-analyzers/overview.mdx @@ -29,6 +29,11 @@ import Admonition from '@theme/Admonition'; # C# Code Analyzers + +This page covers **compile-time C# code analyzers** that catch client-code mistakes at build time. +If you're looking for **text-analysis analyzers** used in index definitions for full-text search (tokenizers, stemmers, language analyzers), see [Using Analyzers in Indexes](../../indexes/using-analyzers.mdx). + + RavenDB ships a set of C# code analyzers embedded in the `RavenDB.Client` NuGet package. They run at compile time inside any project that references the client and catch common misuse patterns before tests, staging, or production. ## Distribution From c61e42893f9718a7c3a210d16255c21d1e6a7b21 Mon Sep 17 00:00:00 2001 From: Szymon Kulec Date: Wed, 13 May 2026 13:09:03 +0200 Subject: [PATCH 5/5] RavenDB-26449: cleanup related articles --- docs/client-api/csharp-code-analyzers/overview.mdx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/client-api/csharp-code-analyzers/overview.mdx b/docs/client-api/csharp-code-analyzers/overview.mdx index 3203ab27d1..256d987248 100644 --- a/docs/client-api/csharp-code-analyzers/overview.mdx +++ b/docs/client-api/csharp-code-analyzers/overview.mdx @@ -3,18 +3,10 @@ title: "C# Code Analyzers" sidebar_label: Overview sidebar_position: 0 see_also: - - title: "Creating and Deploying Indexes" - link: "indexes/creating-and-deploying" - source: "docs" - path: "Indexes" - title: "Query Overview" link: "querying/overview" source: "docs" path: "Querying" - - title: "Document Query" - link: "client-api/session/querying/document-query/what-is-document-query" - source: "docs" - path: "Client API > Session > Querying" - title: "Performing Operations Lazily" link: "client-api/session/how-to/perform-operations-lazily" source: "docs"