feat: bridge a LINQ query into an aggregate with ToAggregateFluent - #2
Open
ahmet-cetinkaya wants to merge 2 commits into
Open
feat: bridge a LINQ query into an aggregate with ToAggregateFluent#2ahmet-cetinkaya wants to merge 2 commits into
ahmet-cetinkaya wants to merge 2 commits into
Conversation
A LINQ query applies query filters and translates joins on its own, but its result is an IQueryable, so it cannot be handed to APIs that accept only an IAggregateFluent. Translate and optimize the query the same way executing it would, then wrap the resulting stages in a fluent over the source collection.
The first version re-derived the preprocess/translate/optimize sequence by hand. That duplicated ExpressionToExecutableQueryTranslator, so an upstream change to the driver's translation path would leave this producing different stages while still compiling. Call that translator instead, and adapt the pipeline's output serializer the way ExecutableQuery does rather than letting a failed cast fall back to the registry default, which would read results from the wrong elements. The stage assertions compared the method against another call into the same translator, so they held no matter what the pipeline looked like; they now assert concrete stages. A grouping query pins the optimizer, whose absence the old tests could not detect. The tests no longer need a running server.
kerem-acer
approved these changes
Aug 10, 2026
ahmet-cetinkaya
marked this pull request as ready for review
August 10, 2026 11:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚀 Context
Mongo koleksiyonu üzerinde yazılan bir LINQ sorgusu pek çok şeyi kendiliğinden hallediyor: query filter'lar otomatik uygulanıyor, başka bir queryable'a yapılan
joinise$lookup'a çevriliyor — inner tarafındakiWheredahil, ki o filtre$lookup'ın kendi sub-pipeline'ının içine yerleşiyor.Son kısım multi-tenant kod için kritik.
$lookup, MongoDB içinde hedef koleksiyona kendi okumasını açıyor; dolayısıyla dış pipeline'daki tenant$match'i oraya hiç ulaşmıyor. Elle yazıldığında tenant kuralının sub-pipeline'a manuel kopyalanması gerekiyor — ve elle kopyalanan bir kural zamanla asıl kuraldan sapabilir. LINQ ile yazıldığında filtre join'in içine kendiliğinden giriyor.Asıl sorun geri dönen tipte. LINQ sorgusu bir
IQueryableüretiyor ve yalnızcaIAggregateFluentkabul eden API'ler bunu tüketemiyor:graph LR A["LINQ sorgusu<br/>filter + join otomatik"] --> B["IQueryable<T>"] C["Aggregate().Lookup(...)<br/>her şey elle"] --> D["IAggregateFluent<T>"] B -.->|köprü yok| D D --> E["IAggregateFluent alan<br/>API'ler"]Her iki biçim de tel üzerinde aynı
$aggregatekomutuna dönüşüyor — driver'ın LINQ provider'ı da tıpkı fluent API gibi arka planda bir pipeline kuruyor. Ancak bu çeviriyi yapan tipler (MongoQueryProvider,TranslatedPipeline,CollectionAggregateFluent)internalolduğu için dönüşüm driver dışından yazılamıyor.Köprü olmadan,
IAggregateFluentihtiyacı olan bir çağıran LINQ'i bırakıp sorguyu elle yeniden kurmak zorunda kalıyor: field path'leri,$exprkorelasyonu, unwind seçenekleri ve LINQ'in kendiliğinden taşıyacağı tenant$match'i yeniden türetmek gerekiyor.⚙️ Implementation Details
Tek bir public extension method ekliyor:
Kritik nokta: çeviriyi kendisi yapmıyor, driver'ın kendi çeviri girişini çağırıyor. Sıralamayı elle tekrarlamak yerine
ExpressionToExecutableQueryTranslator.Translatekullanılıyor, çünkü preprocess/translate/optimize adımlarını burada kopyalamak upstream bir değişiklikte sessiz ayrışma üretirdi — kod derlenmeye devam eder, sadece farklı stage'ler gönderirdi.flowchart TD A["IQueryable<TResult>"] --> B{"Provider<br/>MongoQueryProvider<TDocument> mı?"} B -->|hayır| X["ArgumentException"] B -->|evet| C{"Koleksiyonu var mı?"} C -->|hayır| X C -->|evet| D["ExpressionToExecutableQueryTranslator.Translate<br/><i>driver'ın kendi yolu: preprocess + translate + optimize</i>"] D --> E["Render → BsonDocument[]"] E --> F["Output serializer'ı TResult'a uyarla"] F --> G["BsonDocumentStagePipelineDefinition"] G --> H["CollectionAggregateFluent"]Gözden kaçması kolay ayrıntılar:
asile yumuşak cast yapıpnull'a düşmek, registry'nin varsayılan serializer'ının devreye girmesine ve sonuçların yanlış elemanlardan okunmasına yol açardı. Bunun yerineExecutableQuery.GetOutputSerializerile aynı dört durum ele alınıyor: tam eşleşme, nullable sarma, downcast, ve uyumsuzluktaNotSupportedException.AggregateOptionsprovider'dan taşınıyor, böylece transaction içinde kurulan bir sorgu transaction içinde kalıyor.TDocumentverildiğinde mesaj bunu açıkça söylüyor ("a MongoDB IQueryable over a different document type"), Mongo olmayan bir kaynakla karıştırmıyor. Veritabanı üzerine kurulmuş queryable için ayrı mesaj var.Note
TDocument,IQueryable<TResult>üzerinden çıkarılamıyor — koleksiyonun döküman tipi yalnızca provider'ın içinde var — bu yüzden her iki tip argümanı da çağrı yerinde açıkça veriliyor:query.ToAggregateFluent<Partner, PartnerRow>(). Yanlış verilmesi derleme hatası değil, çalışma zamanında açıklayıcı birArgumentExceptionüretiyor.Verification
QueryableToAggregateFluentTestsiçinde dokuz test, hepsi geçiyor — çalışan bir sunucu gerektirmeden, 154 ms:$match/$sort/$projectstage'leriGroupJoinWhere,$lookup.pipelineiçinde kalıyor$sum'a iniyorAggregateOptionsArgumentException+ParamName+ mesajArgumentException+ ayırt edici mesajnullkaynakArgumentNullExceptionOptimizer testi mutasyonla doğrulandı: optimize adımı atlandığında iki test kırılıyor. Optimize edilmemiş halde bir gruplama tüm dökümanları
$pushile topluyor:{ "$group": { "_id": { "$getField": { "field": "Category", "input": "$$ROOT" } }, "_elements": { "$push": "$$ROOT" } } }Optimizer bunu sunucu tarafı toplamaya indiriyor:
{ "$group": { "_id": "$Category", "__agg0": { "$sum": "$Price" } } }Why this shape
Değerlendirilip elenen alternatifler:
📋 Checklist for Reviewer
TreatWarningsAsErrorsile derleniyor, 0 warning).Important
Bunu başka bir repodan kullanmak için yayımlanmış bir paket sürümü gerekiyor — metot
MongoDB.Driver 3.5.2içinde yok.