Skip to content

feat: bridge a LINQ query into an aggregate with ToAggregateFluent - #2

Open
ahmet-cetinkaya wants to merge 2 commits into
mainfrom
feat/queryable-to-aggregate-fluent
Open

feat: bridge a LINQ query into an aggregate with ToAggregateFluent#2
ahmet-cetinkaya wants to merge 2 commits into
mainfrom
feat/queryable-to-aggregate-fluent

Conversation

@ahmet-cetinkaya

@ahmet-cetinkaya ahmet-cetinkaya commented Aug 10, 2026

Copy link
Copy Markdown

🚀 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 join ise $lookup'a çevriliyor — inner tarafındaki Where dahil, 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ızca IAggregateFluent kabul eden API'ler bunu tüketemiyor:

graph LR
    A["LINQ sorgusu<br/>filter + join otomatik"] --> B["IQueryable&lt;T&gt;"]
    C["Aggregate().Lookup(...)<br/>her şey elle"] --> D["IAggregateFluent&lt;T&gt;"]
    B -.->|köprü yok| D
    D --> E["IAggregateFluent alan<br/>API'ler"]
Loading

Her iki biçim de tel üzerinde aynı $aggregate komutuna 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) internal olduğu için dönüşüm driver dışından yazılamıyor.

Köprü olmadan, IAggregateFluent ihtiyacı olan bir çağıran LINQ'i bırakıp sorguyu elle yeniden kurmak zorunda kalıyor: field path'leri, $expr korelasyonu, 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:

public static IAggregateFluent<TResult> ToAggregateFluent<TDocument, TResult>(
    this IQueryable<TResult> source)

Kritik nokta: çeviriyi kendisi yapmıyor, driver'ın kendi çeviri girişini çağırıyor. Sıralamayı elle tekrarlamak yerine ExpressionToExecutableQueryTranslator.Translate kullanı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&lt;TResult&gt;"] --> B{"Provider<br/>MongoQueryProvider&lt;TDocument&gt; 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"]
Loading

Gözden kaçması kolay ayrıntılar:

  • Output serializer uyarlanıyor, düşürülmüyor. Translator'ın serializer'ı, render edilen stage'lerin gerçekte ürettiği şekli tanımlıyor. as ile yumuşak cast yapıp null'a düşmek, registry'nin varsayılan serializer'ının devreye girmesine ve sonuçların yanlış elemanlardan okunmasına yol açardı. Bunun yerine ExecutableQuery.GetOutputSerializer ile aynı dört durum ele alınıyor: tam eşleşme, nullable sarma, downcast, ve uyumsuzlukta NotSupportedException.
  • Sorgu çalıştırılmıyor. Burada yalnızca çeviri yapılıyor; ne zaman çalıştırılacağına çağıran karar veriyor.
  • Session ve AggregateOptions provider'dan taşınıyor, böylece transaction içinde kurulan bir sorgu transaction içinde kalıyor.
  • Hata mesajları ayırt edici. Yanlış TDocument verildiğ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ı bir ArgumentException üretiyor.

Verification

QueryableToAggregateFluentTests içinde dokuz test, hepsi geçiyor — çalışan bir sunucu gerektirmeden, 154 ms:

Test Neyi kilitliyor
Basit projection Beklenen $match / $sort / $project stage'leri
Filtreli GroupJoin Inner Where, $lookup.pipeline içinde kalıyor
Pipeline optimizer Grouping sorgusu sunucu tarafı $sum'a iniyor
Output serializer Translator'ın serializer'ı korunuyor, registry default'una düşmüyor
AggregateOptions Provider'ın seçenekleri fluent'a taşınıyor
Boş pipeline Değiştirilmemiş queryable sıfır stage üretiyor
Mongo olmayan kaynak ArgumentException + ParamName + mesaj
Veritabanı queryable'ı ArgumentException + ayırt edici mesaj
null kaynak ArgumentNullException

Optimizer 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ı $push ile 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:

  • Çeviri sırasını burada tekrarlamak — ilk hâli buydu. Derlenmeye devam edip farklı stage üretebildiği için terk edildi; artık driver'ın kendi girişi çağrılıyor.
  • Internal tipleri public yapmak — gerçekte ihtiyaç duyulan tek metoda kıyasla çok daha geniş bir API yüzeyi taahhüdü.
  • Çağrı yerinde reflection — çalışır, ancak çağıranları herhangi bir driver güncellemesinde değişebilecek internal isimlere bağlar ve derleyici kontrolü sağlamaz.

📋 Checklist for Reviewer

  • Testler yerelde geçti (9/9, sunucu gerekmiyor).
  • Commit geçmişi temiz ve açıklayıcı.
  • Dokümantasyon güncel — public metot üzerinde XML docs.
  • Kod kalite standartları sağlandı (TreatWarningsAsErrors ile 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.2 içinde yok.

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.
@ahmet-cetinkaya ahmet-cetinkaya added the enhancement New feature or request label Aug 10, 2026
@ahmet-cetinkaya ahmet-cetinkaya self-assigned this Aug 10, 2026
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.
@ahmet-cetinkaya
ahmet-cetinkaya marked this pull request as ready for review August 10, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants