Skip to content

CSHARP-5992: Add LINQ translation benchmark suite#2004

Open
adelinowona wants to merge 17 commits into
mongodb:mainfrom
adelinowona:csharp5992
Open

CSHARP-5992: Add LINQ translation benchmark suite#2004
adelinowona wants to merge 17 commits into
mongodb:mainfrom
adelinowona:csharp5992

Conversation

@adelinowona

@adelinowona adelinowona commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a 9-benchmark LinqBench suite that exercises the LINQ-to-aggregation translation layer in isolation (no queries executed at benchmark time), plus a LinqEndToEnd suite that compares LINQ vs hand-built BsonDocument query plans end-to-end. Wires LinqBench into the perf-job category filter and the composite-score path, giving us a defensible signal when the translator (especially SerializerFinder, AstSimplifier, and the method-call sub-translators) moves.

Cross-commit runs against pre-/post-SerializerFinder and an in-flight optimization PR (#1961) show the suite catches the regressions and improvements we'd want it to — see Validation.

Motivation

The driver has spec-driven benchmarks for I/O-heavy patterns (Find, BulkWrite, GridFS, BSON encode/decode) but nothing for the LINQ translator. As internal paths shift — SerializerFinder overhauls, AstSimplifier changes, new visitor support — we have no signal on translation-cost movement. When CSHARP-5572 introduced SerializerFinder (#1700), we couldn't quantify what it cost. This closes that gap.

What this adds

File Purpose
benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqTranslationBenchmark.cs 9-benchmark translation suite (filter / field / projection / update / IQueryable entry points)
benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs 12 LINQ-vs-raw end-to-end benchmarks (6 patterns × {LINQ, Raw}) against a mongod
benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqBenchmarkDataTypes.cs Shared POCO models for both LINQ suites
benchmarks/MongoDB.Driver.Benchmarks/Linq/README.md Suite docs: full benchmark inventory and how to interpret
benchmarks/MongoDB.Driver.Benchmarks/DriverBenchmarkCategory.cs New LinqBench and ExcludeFromComposite consts; LinqBench and BulkWriteBench added to AllCategories. ExcludeFromComposite keeps the end-to-end benchmarks out of the LinqBench composite while still running them
benchmarks/MongoDB.Driver.Benchmarks/BenchmarkResult.cs + Exporters/*.cs Scores time-throughput benchmarks as operations/second; composite derives its unit from members and skips ExcludeFromComposite benchmarks
evergreen/run-perf-tests.sh Adds LinqBench to the --anyCategories filter

Cedar/SPS auto-discovers new composite categories — no dashboard config required.

Design decisions

  • Translation-only as the primary suite. LinqTranslationBenchmark calls translator entry points directly (LinqProviderAdapter.TranslateExpressionTo*, ExpressionToExecutableQueryTranslator.Translate); no queries run at benchmark time, isolating translator regressions from network and serialization noise. (Caveat: QueryablePipeline and GroupByAggregation need a MongoQueryProvider<T>, obtained from collection.AsQueryable() on a real MongoClient in [GlobalSetup]; its background cluster monitor is the only DB-side activity, expected below the 2-4% drift floor.) End-to-end coverage lives in LinqEndToEndBenchmark; it shares the LinqBench category so it runs in the perf job, and is marked ExcludeFromComposite so it stays out of the composite.
  • Representative user queries, not a visitor matrix. An earlier proposal organized benchmarks by which SerializerFinderVisit*.cs file they exercised; reframed to "what users actually write." Matrix coverage stays an option for targeted gaps.
  • LinqBench does not cross-tag with the spec categories (DriverBench, BsonBench, etc.), keeping LINQ numbers out of the spec composite averages. It lands in AllCategories so its own composite is emitted.
  • BulkWriteBench composite bundled here. Previously excluded with a "not part of the benchmarking spec" comment; team wanted its composite tracked, done here to avoid a tiny standalone PR.
  • [MemoryDiagnoser] on everything. Allocation regressions matter independently of time and surface earlier than time regressions on noisy hardware.

Benchmark inventory

Translation suite: 9 benchmarks across filter (4), field (1), projection (1), update (1), and IQueryable (2) entry points. End-to-end suite: 12 benchmarks — 6 patterns (MultiFieldSearch, OrFilter, GroupBy, Projection, InFilter, PagedQuery) each run LINQ vs hand-built raw. The full per-benchmark breakdown (patterns, translator paths exercised) lives in Linq/README.md.

The e2e suite seeds 500 documents with secondary indexes on Status, CreatedAt, and ShippingAddress.City in [GlobalSetup]; each LINQ/Raw pair renders to byte-equivalent BSON (verified via LinqProviderAdapter), so the LINQ−Raw delta isolates translator + provider overhead from query-shape differences.

Validation

Five lines of evidence that the suite produces actionable signal.

1. Within-run noise

Across all perf-hw runs (n=10 each, multiple commits), BDN-reported within-run StdDev is <1% on most benchmarks (typically 0.2-0.9%), with the fastest micro-benchmarks at 0.3-1.4%. Each individual run is a well-converged measurement.

2. Selectivity — targeted regression injection

Thread.SpinWait(300) (~10 µs on M1) injected into four translator code paths in turn:

Injection point Expected affected benchmarks
SerializerFinder.FindSerializers() All benchmarks (universal path)
GetItemMethodToFilterFieldTranslator FieldSelection only
NotExpressionToFilterTranslator MultiFieldSearch (Not), OrFilter (chains Comparison dispatch)
GroupByMethodToPipelineTranslator GroupByAggregation only

Each injection moved only the benchmarks that should have moved — clean per-path selectivity, so when something regresses the benchmarks that move tell you which translator moved.

3. Cross-run drift on the perf-job hardware (n=10 on rhel90-dbx-perf-large)

.NET 8.0, X64 RyuJit, single perf-task invocation on the same host.

Benchmark Min Median Max Range% CV% Within-run StdDev%
MultiFieldSearch 494.0 µs 497.2 µs 507.8 µs 2.8% 1.03% 0.23%
OrFilter 13.3 µs 13.5 µs 13.7 µs 3.1% 1.01% 0.23%
BatchLookup 144.9 µs 145.8 µs 148.1 µs 2.2% 0.74% 0.88%
ArrayElementQuery 177.4 µs 179.4 µs 181.4 µs 2.2% 0.77% 0.51%
FieldSelection 5,610 ns 5,795 ns 6,025 ns 7.1% 2.30% 0.32%
AggregationProjection 406.5 µs 412.6 µs 420.1 µs 3.3% 1.07% 0.47%
UpdatePipeline 95.5 µs 96.3 µs 98.4 µs 3.0% 1.06% 0.59%
QueryablePipeline 874.5 µs 890.7 µs 913.6 µs 4.4% 1.38% 0.52%
GroupByAggregation 489.7 µs 495.1 µs 506.6 µs 3.4% 0.94% 0.77%

Every benchmark but FieldSelection lands in 2-4.5% range, CV ≤1.5% — a ~5× compression of the M1 drift bands characterized during development. FieldSelection (~6µs) drifts wider (7.1%), fast enough that small absolute drift looks proportionally large. Sub-10% deltas are individually resolvable, which matters for the optimization comparison below.

4. Cross-commit reality check

Transplanted onto pinned commits and run on rhel90-dbx-perf-large (n=10 per commit), the suite caught both a known historical regression and an in-flight optimization:

  • SerializerFinder introduction (CSHARP-5572: Implement new SerializerFinder #1700) — parent (46640eac98) vs the merge (59c9d34180): every benchmark moved well above its drift band, most 2-3× slower. UpdatePipeline was a +626% time / +898% allocation outlier because TranslateExpressionToSetStage runs SerializerFinder on the un-preprocessed lambda, unlike the other entry points which preprocess first (root cause in the follow-up below). Confirms the suite surfaces real translator regressions.
  • Optimization CSHARP-5959: Replace switch by method name in SerializerFinderVisitMethodCall with lookup table with MethodInfo as a key #1961 (SerializerFinderVisitMethodCall switch → MethodInfo-keyed lookup), base 66780341e7 vs head 54973d039a — measurable allocation wins on the method-call / IQueryable benchmarks (ArrayElementQuery -12.4%, QueryablePipeline -4.5%, BatchLookup / GroupByAggregation -3.3%) with smaller time effects at the edge of the drift bands. The suite resolves what kind of change this is — an allocation reduction — which was invisible at M1 noise levels.

5. End-to-end overhead — Atlas dev cluster, perf-hardware, n=10

Six patterns run twice each (LINQ-translated vs hand-built BsonDocument / pipeline), 500 docs with secondary indexes, against a live Atlas dev cluster from the perf host. Each LINQ/Raw pair renders to byte-equivalent BSON, so the LINQ−Raw delta reflects translator and provider overhead, not query-shape differences.

Pattern LINQ median Raw median LINQ−Raw LINQ/Raw Translator share LINQ alloc Raw alloc Alloc ratio
MultiFieldSearch 1,966 µs 1,185 µs +782 µs 1.66× 39.7% 74.2 KB 43.6 KB 1.70×
OrFilter 7,321 µs 7,230 µs +91 µs 1.01× 1.2% 719.1 KB 711.1 KB 1.01×
GroupBy 2,034 µs 1,476 µs +558 µs 1.38× 27.4% 65.4 KB 20.6 KB 3.18×
Projection 2,160 µs 1,051 µs +1,109 µs 2.05× 51.3% 124.8 KB 35.5 KB 3.52×
InFilter 1,100 µs 784 µs +315 µs 1.40× 28.7% 41.5 KB 28.8 KB 1.44×
PagedQuery 1,645 µs 1,213 µs +432 µs 1.36× 26.3% 63.3 KB 34.8 KB 1.82×

Translator share is the fraction of user-visible LINQ time that disappears if you write raw BsonDocument instead — an upper bound on translator cost (the delta also includes provider overhead like cursor construction and command serialization).

  • Projection-heavy patterns (Projection 51%, MultiFieldSearch 40%): translator is ~40-50% of user-visible latency on indexed selective queries; a 10% translator regression is a ~5% user-visible one.
  • Filter/aggregation patterns (GroupBy 27%, InFilter 29%, PagedQuery 26%): translator is ~25-30%; meaningful server-side work ($group, $in, sort-skip-limit) partially offsets it.
  • Broad scans (OrFilter 1.2%): translator is ~1% because the 4-way OR matches many documents and serializes ~700 KB, so a 10% translator regression is invisible to users.
  • Allocation ratios isolate LINQ-side overhead: Projection 3.5×, GroupBy 3.2× — projected-type-serializer and IGroupingSerializer construction is allocation-heavy, and catches translator-side allocation regressions even when network time masks the time delta.

Caveat: Atlas dev cluster across the internet from the perf host; per-run absolute times range ±15-30% (up to ±50% where server time dominates), but the translator-share ratios are more stable because LINQ and Raw on the same iteration see correlated network noise. Single-batch result, 500 docs.

Regression-alert thresholds (perf-hardware-calibrated)

Calibrated to observed drift on rhel90-dbx-perf-large:

Bucket Threshold Benchmarks Rationale
Tight 8% MultiFieldSearch, BatchLookup, ArrayElementQuery, AggregationProjection, UpdatePipeline, QueryablePipeline, GroupByAggregation, OrFilter Observed range 2.2–4.4%; 8% gives ~2× headroom.
Wider 12% FieldSelection ~6µs benchmark; observed range 7.1%.

Allocation thresholds should be even tighter — observed allocation drift is 0-1.2%.

Follow-ups (not in this PR)

  • File a ticket for the TranslateExpressionToSetStage preprocessing asymmetry surfaced above (SerializerFinder runs on the un-preprocessed tree; UpdatePipeline +626% time, +898% alloc). The design is intentional — dispatch pattern-matches on NewExpression/MemberInitExpression, which PartialEvaluator would collapse if applied at the top — so any fix must preserve that dispatch shape. Not a one-line change.
  • Sub-3% time-delta detection on real changes hasn't been measured end-to-end (current experiments validate large deltas and 3-12% alloc deltas); the perf-hw drift bands suggest it's feasible.
  • Coverage gaps: Lookup/Join, Distinct, SelectMany, Cast, $expr fallback paths.
  • Quantify the background cluster-monitor's noise contribution to QueryablePipeline/GroupByAggregation (expected sub-1%, absorbed by the 2-4% drift floor).
  • Runtime equivalence guard for the e2e suite: assert each LINQ expression's translated BSON equals its pre-built Raw doc in [GlobalSetup], so a future translator shape change fails the benchmark loudly instead of silently shifting the share numbers.

@adelinowona adelinowona added the maintenance Non-code maintenance (deps, docs, configs, etc.). label May 19, 2026
@adelinowona adelinowona force-pushed the csharp5992 branch 3 times, most recently from ea1069d to 148d1de Compare May 20, 2026 18:15
15 benchmarks covering filter, projection, and IQueryable composition
translation paths. New LinqBench category added to AllCategories and
the perf-test runner. BulkWriteBench also added to AllCategories.
Document benchmark inventory, code path coverage, interpretation
guidance, and provisional thresholds. Record targeted injection test
results validating selective sensitivity of each benchmark to its
target translator code path.
PartialEvaluator injection test showed OrChainFilter is still
affected (evaluator traverses all expressions). Reframed as a
sensitivity amplifier rather than diagnostic isolator.
…methods

LinqBench uses translations/second instead of MB/s since there is no
data throughput to measure. Add Unit and MetricName to BenchmarkResult
so exporters label scores correctly. Change all benchmark methods to
return their values to prevent JIT dead-code elimination.
The composite score loop was using default MB/s labels for all
categories including LinqBench. Now correctly labels LinqBench
composites as translations_per_second.
…d-to-end benchmarks; update regression thresholds

- Redesign LinqTranslationBenchmark.cs: 15 individual feature benchmarks → 10
  representative user queries covering distinct translator code paths
  (MultiFieldSearch, OrStatusFilter, BatchLookup, ArrayElementQuery,
  FieldSelection, AggregationProjection, ProjectionSentinel, UpdatePipeline,
  QueryablePipeline, GroupByAggregation)
- Add LinqEndToEndBenchmark.cs: one-off characterization of translation overhead
  vs pre-built BsonDocument queries on a live collection; not wired into CI
- Update README regression thresholds based on 7-run M1 Max drift characterization:
  tight bucket (15%) for MultiFieldSearch/UpdatePipeline/BatchLookup/ArrayElementQuery;
  wider bucket (30%) for OrStatusFilter/FieldSelection/AggregationProjection/
  QueryablePipeline/GroupByAggregation

@BorisDog BorisDog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look very good overall.

Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs
{
return _collection.Find(x =>
x.Status == _statusFilter &&
x.CustomerName.StartsWith(_prefix) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does StartsWith create the exact same regex as in the raw version?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might makes sense to made a first translation somewhere in GlobalSetup and compare the produced MQL. So if we will change the translation in future the Benchmark will throw.

{
private const string DatabaseName = "linqbench";
private const string CollectionName = "orders";
private const int SeedCount = 500;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider creating indexes in the database such that server time is minimized and translation time changes will be more apparent.

public List<BsonDocument> GroupByLinq()
{
return _collection.Aggregate()
.Group(x => x.Status, g => new { Status = g.Key, Count = g.Count(), TotalRevenue = g.Sum(x => x.Total) })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is projection to an anonymous type here, followed by creation of the BSON document. The raw example does not project to an anonymous type.

…quivalence fixes; rename OrStatusFilter to OrFilter
@adelinowona adelinowona changed the title WIP: Add LINQ translation benchmark suite CSHARP-5992: Add LINQ translation benchmark suite May 28, 2026
@adelinowona adelinowona marked this pull request as ready for review May 28, 2026 15:59
@adelinowona adelinowona requested a review from a team as a code owner May 28, 2026 15:59
Copilot AI review requested due to automatic review settings May 28, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new LINQ-focused benchmark suite to the driver benchmarks project, enabling perf-job tracking and composite scoring for LINQ translation performance (plus an optional end-to-end comparison suite).

Changes:

  • Introduces LinqTranslationBenchmark (translation-only, no query execution) and LinqEndToEndBenchmark (LINQ vs raw query plans with real DB execution).
  • Wires new LinqBench category into perf-job filtering and composite-score export (including score units/metric names).
  • Extends benchmark category constants and composite export output to include LinqBench (and now BulkWriteBench) in AllCategories.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
evergreen/run-perf-tests.sh Adds LinqBench to Evergreen perf category filter.
benchmarks/MongoDB.Driver.Benchmarks/Linq/README.md Documents the LINQ benchmark inventory and interpretation guidance.
benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqTranslationBenchmark.cs Adds translation-focused benchmarks across filter/field/projection/update/IQueryable entry points.
benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs Adds end-to-end LINQ vs raw benchmarks that seed data and run against a live server.
benchmarks/MongoDB.Driver.Benchmarks/Exporters/LocalExporter.cs Emits per-category and per-benchmark units (MB/s vs translations/s).
benchmarks/MongoDB.Driver.Benchmarks/Exporters/EvergreenExporter.cs Emits per-category and per-benchmark metric names for Evergreen (MB/s vs translations/s).
benchmarks/MongoDB.Driver.Benchmarks/DriverBenchmarkCategory.cs Adds LinqBench and includes it (and BulkWriteBench) in composite category list.
benchmarks/MongoDB.Driver.Benchmarks/BenchmarkResult.cs Adds unit/metric metadata and computes translations/s scoring for LinqBench.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/README.md Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/README.md Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/README.md Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs
{
return _collection.Find(x =>
x.Status == _statusFilter &&
x.CustomerName.StartsWith(_prefix) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might makes sense to made a first translation somewhere in GlobalSetup and compare the produced MQL. So if we will change the translation in future the Benchmark will throw.

Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs

@sanych-sun sanych-sun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs Outdated
};

// ids.Contains(x.Id) translates to { "_id": { "$in": [...] } } since Id maps to _id by convention.
_inFilter = new BsonDocument("_id", new BsonDocument("$in", new BsonArray(_lookupIds.Select(id => (BsonValue)id))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we _lookupIds.Select( ...) be evaluated once? So it doesn't contributed to benchmark time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good instinct, but it's already setup-only. That _lookupIds.Select(...) is inside PreBuildQueries(), which runs from Setup() under [GlobalSetup] — so the $in array is materialized into _inFilter once before any measured iteration, not per-invocation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, might bad :)

Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqTranslationBenchmark.cs Outdated
…osite unit from members

Dispatch BenchmarkResult scoring on model rather than category: byte-throughput
(BsonBench data or a present BenchmarkDataSetSize param) yields MB/s via a shared
helper, everything else is time-throughput scored as operations/second. This gives
the end-to-end LINQ benchmark an honest score and removes the NullReferenceException
that hit any benchmark lacking BenchmarkDataSetSize.

Mark ProjectionSentinel ExcludeFromComposite so its ~17ns fast-path timing no longer
dominates the averaged LinqBench composite while it still runs and is reported
individually.

CalculateComposite now derives the composite's unit from its members instead of
branching on category, dropping the duplicated unit ternary from both exporters.
Put LinqEndToEndBenchmark in the LinqBench category and mark it ExcludeFromComposite.
It runs in the perf job (LinqBench is already selected) and is reported individually,
but stays out of the LinqBench composite, whose translation-only members are orders of
magnitude faster. These benchmarks measure the translator's share of user-visible
latency under realistic result sizes; tracking them over time shows how that share
shifts as serialization and other round-trip costs change.

Remove ProjectionSentinel: the x => x fast path it guarded is already covered
deterministically by CSharp4742Tests, which assert the identity projection renders a
null document. A timing benchmark with a 100% threshold is a weaker guard of the same
behavior.
…the README

Retitle to cover both suites and scope the no-queries statement to the translation
suite. Add an End-to-End Benchmarks section explaining what that suite measures
(the translator's share of user-visible latency), that translator regressions are
caught by the translation suite rather than here, and that it is tracked as a trend
rather than gated.

Remove the Code Path Coverage section, including its claim of validation by targeted
injection tests that do not exist. Drop the ProjectionSentinel references and specific
test-hardware names.
…trend wording

The repo enforces no regression thresholds — detection runs downstream on the
emitted results — so a provisional, locally-characterized threshold table in the
README documented nothing the code controls and duplicated the calibrated bands in
the PR. Reword the end-to-end note to recommend reading the series as a trend rather
than asserting it is configured that way.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread benchmarks/MongoDB.Driver.Benchmarks/BenchmarkHelper.cs Outdated
Comment thread benchmarks/MongoDB.Driver.Benchmarks/Linq/LinqEndToEndBenchmark.cs
- Empty-category composites fall back to the category's normal unit (LinqBench →
  ops/s, otherwise MB/s) so a composite's metric name is stable across runs whether
  or not the category ran, instead of always reporting operations_per_second.
- Replace DateTime.UtcNow in the update-pipeline benchmark expression with a fixed
  DateTime, removing a per-invocation clock read from the translation measurement.
- Use the strongly-typed index-key overload for ShippingAddress.City instead of a
  string, matching the sibling keys.

@BorisDog BorisDog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

};

// ids.Contains(x.Id) translates to { "_id": { "$in": [...] } } since Id maps to _id by convention.
_inFilter = new BsonDocument("_id", new BsonDocument("$in", new BsonArray(_lookupIds.Select(id => (BsonValue)id))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, might bad :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Non-code maintenance (deps, docs, configs, etc.).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants