Skip to content

MongoDB

The ExpressiveSharp.MongoDB package integrates ExpressiveSharp with the official MongoDB.Driver LINQ provider. Modern C# syntax — null-conditional operators, switch expressions, pattern matching — and [Expressive] members are translated into MongoDB aggregation pipelines.

Installation

bash
dotnet add package ExpressiveSharp.MongoDB

Depends on ExpressiveSharp (core runtime) and MongoDB.Driver 3.x.

Basic Setup

Call .AsExpressive() on an IMongoCollection<T> to get an IExpressiveMongoQueryable<T>:

csharp
using ExpressiveSharp.MongoDB.Extensions;
using MongoDB.Driver;

var client = new MongoClient("mongodb://localhost:27017");
var db = client.GetDatabase("shop");

var customers = db.GetCollection<Customer>("customers").AsExpressive();
var orders = db.GetCollection<Order>("orders").AsExpressive();

That's it. Both modern syntax and [Expressive] member expansion are now active:

db
    .Customers
    .Where(c => c.Email != null && c.Orders.Count() > 5)
    .Select(c => new { c.Name, OrderCount = c.Orders.Count() })
SELECT "c"."Name", (
    SELECT COUNT(*)
    FROM "Orders" AS "o0"
    WHERE "c"."Id" = "o0"."CustomerId") AS "OrderCount"
FROM "Customers" AS "c"
WHERE "c"."Email" IS NOT NULL AND (
    SELECT COUNT(*)
    FROM "Orders" AS "o"
    WHERE "c"."Id" = "o"."CustomerId") > 5
SELECT c."Name", (
    SELECT count(*)::int
    FROM "Orders" AS o0
    WHERE c."Id" = o0."CustomerId") AS "OrderCount"
FROM "Customers" AS c
WHERE c."Email" IS NOT NULL AND (
    SELECT count(*)::int
    FROM "Orders" AS o
    WHERE c."Id" = o."CustomerId") > 5
SELECT [c].[Name], (
    SELECT COUNT(*)
    FROM [Orders] AS [o0]
    WHERE [c].[Id] = [o0].[CustomerId]) AS [OrderCount]
FROM [Customers] AS [c]
WHERE [c].[Email] IS NOT NULL AND (
    SELECT COUNT(*)
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId]) > 5
playground.customers.Aggregate([
    {
         "$match" : {
             "Email" : { "$ne" : null },
            "Orders.5" : { "$exists" : true } 
        } 
    },
    {
         "$project" : {
             "Name" : "$Name",
            "OrderCount" : { "$size" : "$Orders" },
            "_id" : 0 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "nGAPq9D2WsuOKDQzNLtf/8ABAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<T1> __Polyfill_Select_3e61_16_5<T0, T1>(
            this global::ExpressiveSharp.IExpressiveQueryable<T0> source,
            global::System.Func<T0, T1> __func)
        {
            // Source: c => new { c.Name, OrderCount = c.Orders.Count() }
            var i3e6116c5_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "c");
            var i3e6116c5_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_c, typeof(T0).GetProperty("Name", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Name
            var i3e6116c5_expr_3 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_c, typeof(T0).GetProperty("Orders", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Orders
            var i3e6116c5_expr_2 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter)).MakeGenericMethod(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order)), new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_3 });
            var i3e6116c5_expr_4 = typeof(T1).GetConstructors()[0];
            var i3e6116c5_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6116c5_expr_4, new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_1, i3e6116c5_expr_2 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Name"), typeof(T1).GetProperty("OrderCount") });
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6116c5_expr_0, i3e6116c5_p_c);
            return (global::ExpressiveSharp.IExpressiveQueryable<T1>)(object)
                global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                    global::System.Linq.Queryable.Select(
                        (global::System.Linq.IQueryable<T0>)(object)source,
                        __lambda));
        }
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "nGAPq9D2WsuOKDQzNLtf/4cBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> __Polyfill_Where_3e61_15_5(
            this global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> source,
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool> __func)
        {
            // Source: c => c.Email != null && c.Orders.Count() > 5
            var i3e6115c5_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer), "c");
            var i3e6115c5_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_p_c, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer).GetProperty("Email", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Email
            var i3e6115c5_expr_4 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var i3e6115c5_expr_3 = global::System.Linq.Expressions.Expression.Convert(i3e6115c5_expr_4, typeof(string));
            var i3e6115c5_expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6115c5_expr_2, i3e6115c5_expr_3);
            var i3e6115c5_expr_7 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_p_c, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer).GetProperty("Orders", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Orders
            var i3e6115c5_expr_6 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter)).MakeGenericMethod(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order)), new global::System.Linq.Expressions.Expression[] { i3e6115c5_expr_7 });
            var i3e6115c5_expr_8 = global::System.Linq.Expressions.Expression.Constant(5, typeof(int)); // 5
            var i3e6115c5_expr_5 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, i3e6115c5_expr_6, i3e6115c5_expr_8);
            var i3e6115c5_expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, i3e6115c5_expr_1, i3e6115c5_expr_5);
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool>>(i3e6115c5_expr_0, i3e6115c5_p_c);
            return global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                global::System.Linq.Queryable.Where(
                    (global::System.Linq.IQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer>)source,
                    __lambda));
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : global::System.Attribute
    {
        public InterceptsLocationAttribute(int version, string data) { }
    }
}

What AsExpressive() Does

AsExpressive() wraps MongoDB's LINQ provider with ExpressiveSharp's query provider:

  1. Expands [Expressive] member references — walks the expression tree and replaces opaque property/method accesses with the generated expression trees
  2. Applies MongoDB-friendly transformers — strips null-conditional patterns, flattens blocks, normalizes tuple access
  3. Delegates execution to MongoDB's aggregation pipeline — the rewritten tree is handed back to the MongoDB LINQ provider unchanged in shape

No custom MQL is emitted — MongoDB's own translator does all the heavy lifting after ExpressiveSharp has normalized the tree.

[Expressive] Properties Are Unmapped from BSON

ExpressiveSharp provides a MongoDB IClassMapConvention that unmaps every [Expressive]-decorated property from the BSON class map, so the property's backing field is not persisted to documents. This matters most for synthesized properties, which have a writable init accessor and would otherwise be serialized as a real BSON field.

Ordering constraint

MongoDB builds and caches a class map the first time you call IMongoDatabase.GetCollection<T>() for a given T. A convention registered after that call does not apply to the cached map. If any of your document types use [Expressive], register the convention before the first GetCollection<T> call:

csharp
using ExpressiveSharp.MongoDB.Infrastructure;

// At application startup, before any GetCollection<T>:
ExpressiveMongoIgnoreConvention.EnsureRegistered();

var client = new MongoClient(connectionString);
var db = client.GetDatabase("shop");
var customers = db.GetCollection<Customer>("customers");  // class map built now

The convention is also registered automatically when you construct ExpressiveMongoCollection<T> or call collection.AsExpressive() — but only if that happens before any GetCollection<T> call for a type with [Expressive] properties. The explicit EnsureRegistered() call is the most reliable pattern.

Async Methods

All MongoDB async LINQ methods (from MongoQueryable) work with modern syntax via interceptors. They are stubs on IExpressiveMongoQueryable<T> that forward to their MongoQueryable counterparts:

csharp
// Predicate / element access
var exists = await customers.AnyAsync(c => c.Orders.Count() > 0);
var first = await customers.FirstOrDefaultAsync(c => c.Email != null);
var count = await customers.CountAsync(c => c.Country == "US");

// Aggregation
var total = await orders.SumAsync(o => o.Price * o.Quantity);
var avg = await orders.AverageAsync(o => o.Price);

Inspecting the Pipeline

Call .ToString() on an IExpressiveMongoQueryable<T> to see the generated aggregation pipeline without executing it:

csharp
var query = customers
    .Where(c => c.Email != null)
    .Select(c => new { c.Name, c.Email });

Console.WriteLine(query.ToString());

Output:

shop.customers.Aggregate([
    { "$match" : { "Email" : { "$ne" : null } } },
    { "$project" : { "Name" : "$Name", "Email" : "$Email", "_id" : 0 } }
])

Caveats

  • No navigation properties. MongoDB is a document store; [Expressive] members that reach across collections (customer.Orders) assume the related data is embedded as a subdocument. If your schema uses references across collections, project and $lookup explicitly.
  • No cross-field [Expressive] with untracked fields. MongoDB's LINQ provider requires every field referenced in a projection or filter to exist in the document schema. An [Expressive] member that references a non-persisted field won't translate.

Next Steps

Released under the MIT License.