score:1

Whatever you're trying to pass into .Where() method is an expression tree that needs to be translated into MongoDB query language. C# compiler will accept ExpandoObject's methods like SingleOrDefault (extension method) but this will fail in the runtime when such expression tree needs to be translated into Mongo query.

There's nothing special about ExpandoObject when you work with MongoDB. It has to be translated somehow into BSON document (MongoDB format) and for example below code:

dynamic o = new ExpandoObject();
o.dateOfBith = new DateTime(2000, 1, 1);
r.Contents = o;
col.InsertOne(r);

inserts ExpandoObject same way as BsonDocument or Dictionary<string, T> so it simply becomes:

{ "_id" : ObjectId(",,,"), "contents" : { "dateOfBith" : ISODate("2000-01-01T07:00:00Z") } }

in your database.

Knowing that you can build your query using the dot notation. There's also a StringFieldDefinition class you can utilize since you cannot build an expression tree from ExpandoObject in a strongly typed way:

var fieldDef = new StringFieldDefinition<Record, DateTime>("contents.dateOfBith");
var filter = Builders<Record>.Filter.Gt(fieldDef, new DateTime(2000, 1, 1));

var res = col.Find(filter).ToList();

score:1

These kind of Expression transformations doesn't play well with LINQ how I see. Tho if you go down 1 level to define the Field as string, it will play better. Than you can Inject your Where clause to keep IQueryable if you like>

Update (you can have multiple conditions, and different kinds of null checking)

string connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("test");
var records = db.GetCollection<Record>("recordData");
//dynamic content = new ExpandoObject();
//content.dateOfBirth = DateTime.Today;
//records.InsertOne(new Record
//{
//    AppName = "my app", Created = DateTime.Today, CreatedBy = "some user", FormName = "form name",
//    OrganisationId = "organization id", SchemaVersion = 1, Version = "1.1", Contents = content
//});
// first option for multiple conditions:
var filter =
    Builders<Record>.Filter.Lt("contents.dateOfBirth", DateTime.Now) &
    Builders<Record>.Filter.Gt("contents.dateOfBirth", DateTime.Now.AddDays(-10)) &
    // checking if the field is there. Whether the value is null or not.
    Builders<Record>.Filter.Exists("contents.dateOfBirth") &
    // checking if the field is there, and it's not null
    Builders<Record>.Filter.Ne("contents.dateOfBirth", BsonNull.Value);
// second option for multiple conditions
var dateOfBirths = records.AsQueryable().Where(_ => filter.Inject()).Where(x => x.AppName == "my app").ToList();

from there, you can continue your IQueryable syntax.

Update 2

In case of .Where(x => x.AppName.Contains("app")) you'll get

"pipeline" : [
        {
            "$match" : {
                "contents.dateOfBirth" : {
                    "$exists" : true,
                    "$gt" : ISODate("2020-05-15T12:48:14.483+02:00"),
                    "$lt" : ISODate("2020-05-25T12:48:14.480+02:00"),
                    "$ne" : null
                }
            }
        },
        {
            "$match" : {
                "appName" : /app/g
            }
        }
    ]

from profiling the database. So the <string>.Contains(<string>) is implemented in IQueryable as a regular expression filter not in memory.


Related Articles