I'm new to MongoDB so this might be a naive question, yet I have not found any relevant/up to date information by googling around: I am trying to use the MongoDB C# driver (version 2.2.4) to compose a LINQ-based query, one piece at a time, from a received filter POCO object, like this:
IQueryable<BsonDocument> parts = collection.AsQueryable();
if (filter.Name != null)
parts = parts.Where(d => d["Name"].Equals(filter.Name));
// ... etc; I'll then call ToList() to get results ...
Now, one of my filter properties is a string array, meaning that I should match any document whose field Vendor (a string property in the MongoDB document) is equal to any of the strings in the array (like MongoDB native $in: https://docs.mongodb.com/manual/reference/operator/query/in/).
To this end, I tried with Contains (the special case for a 1-sized array is just an optimization):
if (filter.Vendors != null && filter.Vendors.Length > 0)
{
parts = filter.Vendors.Length == 1
? parts.Where(d => d["Vendor"].Equals(filter.Vendors[0]))
: parts.Where(d => filter.Vendors.Contains(d["Vendor"].AsString));
}
This compiles, but throws an ArgumentException: "Expression of type 'MongoDB.Bson.BsonValue' cannot be used for parameter of type 'System.String' of method 'Boolean Contains[String](System.Collections.Generic.IEnumerable`1[System.String], System.String)'".
Looking at http://mongodb.github.io/mongo-csharp-driver/2.2/reference/driver/crud/linq/, there is nothing about Contains or $in; yet, from https://jira.mongodb.org/browse/CSHARP-462 it seems that the driver should now be capable of handling that method.
BTW, the same happens if I slightly change the code to:
parts.Where(d => filter.Vendors.Any(s=> d["Vendor"].Equals(s)));
which does not involve Contains at all. The exception message complains about not being able to use BsonValue for string, yet that BsonValue is right a string. Could anyone suggest a solution?
The exception messages dance around the idea of fully embracing BsonValue to let mongo handle the types instead of trying to cast to string. I got it to work having Vendors as type List<BsonValue>.
class Filter
{
public List<BsonValue> Vendors { get; set; }
}
...
var list = parts.Where(d => filter.Vendors.Contains(d["Vendor"]));
foreach (var document in list)
{
Console.WriteLine(document["Name"]);
}
Another alternative is to map your documents to a C# class instead of using BsonDocument as the collection type.
class MyDocument
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public string Vendor { get; set; }
}
...
var collection = db.GetCollection <MyDocument> ("parts");
Related
This is my MongoDB document structure:
{
string _id;
ObservableCollection<DataElement> PartData;
ObservableCollection<DataElement> SensorData;
...
other ObservableCollection<DataElement> fields
...
other types and fields
...
}
Is there any possibility to retrieve a concatenation of fields with the type ObservableCollection<DataElement>? Using LINQ I would do something like
var query = dbCollection
.AsQueryable()
.Select(x => new {
data = x
.OfType(typeof(ObservableCollection<DataElement>))
.SelectMany(x => x)
.ToList()
});
or alternatively
data = x.Where(y => typeof(y) == typeof(ObservableCollection<DataElement>)
.SelectMany(x => x).ToList()
Unfortunately .Where() and .OfType() do not work on documents, only on queryables/lists, so is there another possibility to achieve this? The document structure must stay the same.
Edit:
After dnickless answer I tried it with method 1b), which works pretty well for getting the fields thy way they are in the collection. Thank you!
Unfortunately it wasn't precisely what I was looking for, as I wanted to be all those fields with that specific type put together in one List, at it would be returned by the OfType or Where(typeof) statement.
e.g. data = [x.PartData , x.SensorData, ...] with data being an ObsverableCollection<DataElement>[], so that I can use SelectMany() on that to finally get the concatenation of all sequences.
Sorry for asking the question unprecisely and not including the last step of doing a SelectMany()/Concat()
Finally I found a solution doing this, but it doesn't seem very elegant to me, as it needs one concat() for every element (and I have more of them) and it needs to make a new collection when finding a non-existing field:
query.Select(x => new
{
part = x.PartData ?? new ObservableCollection<DataElement>(),
sensor = x.SensorData ?? new ObservableCollection<DataElement>(),
}
)
.Select(x => new
{
dataElements = x.part.Concat(x.sensor)
}
).ToList()
In order to limit the fields returned you would need to use the MongoDB Projection feature in one way or the other.
There's a few alternatives depending on your specific requirements that I can think of:
Option 1a (fairly static approach): Create a custom type with only the fields that you are interested in if you know them upfront. Something like this:
public class OnlyWhatWeAreInterestedIn
{
public ObservableCollection<DataElement> PartData { get; set; }
public ObservableCollection<DataElement> SensorData { get; set; }
// ...
}
Then you can query your Collection like that:
var collection = new MongoClient().GetDatabase("test").GetCollection<OnlyWhatWeAreInterestedIn>("test");
var result = collection.Find(FilterDefinition<OnlyWhatWeAreInterestedIn>.Empty);
Using this approach you get a nicely typed result back without the need for custom projections.
Option 1b (still pretty static): A minor variation of Option 1a, just without a new explicit type but a projection stage instead to limit the returned fields. Kind of like that:
var collection = new MongoClient().GetDatabase("test").GetCollection<Test>("test");
var result = collection.Find(FilterDefinition<Test>.Empty).Project(t => new { t.PartData, t.SensorData }).ToList();
Again, you get a nicely typed C# entity back that you can continue to operate on.
Option 2: Use some dark reflection magic in order to dynamically create a projection stage. Downside: You won't get a typed instance reflecting your properties but instead a BsonDocument so you will have to deal with that afterwards. Also, if you have any custom MongoDB mappings in place, you would need to add some code to deal with them.
Here's the full example code:
First, your entities:
public class Test
{
string _id;
public ObservableCollection<DataElement> PartData { get; set; }
public ObservableCollection<DataElement> SensorData { get; set; }
// just to have one additional property that will not be part of the returned document
public string TestString { get; set; }
}
public class DataElement
{
}
And then the test program:
public class Program
{
static void Main(string[] args)
{
var collection = new MongoClient().GetDatabase("test").GetCollection<Test>("test");
// insert test record
collection.InsertOne(
new Test
{
PartData = new ObservableCollection<DataElement>(
new ObservableCollection<DataElement>
{
new DataElement(),
new DataElement()
}),
SensorData = new ObservableCollection<DataElement>(
new ObservableCollection<DataElement>
{
new DataElement(),
new DataElement()
}),
TestString = "SomeString"
});
// here, we use reflection to find the relevant properties
var allPropertiesThatWeAreLookingFor = typeof(Test).GetProperties().Where(p => typeof(ObservableCollection<DataElement>).IsAssignableFrom(p.PropertyType));
// create a string of all properties that we are interested in with a ":1" appended so MongoDB will return these fields only
// in our example, this will look like
// "PartData:1,SensorData:1"
var mongoDbProjection = string.Join(",", allPropertiesThatWeAreLookingFor.Select(p => $"{p.Name}:1"));
// we do not want MongoDB to return the _id field because it's not of the selected type but would be returned by default otherwise
mongoDbProjection += ",_id:0";
var result = collection.Find(FilterDefinition<Test>.Empty).Project($"{{{mongoDbProjection}}}").ToList();
Console.ReadLine();
}
}
I have a very simple query for mongo:
db.items.find( { MyFieldName: { $exists: true, $eq: null } } );
Not that it needs to be explained, but it finds documents which have a MyFieldName and where the value of that field is null. It seems like this would be really simple to do with the C# driver:
var fieldExistsFilter= Builders<BsonDocument>.Filter.Exists("MyFieldName", true);
var fieldValueIsNullFilter = Builders<BsonDocument>.Filter.Eq("MyFieldName", null);
However the second filter, fieldValueIsNullFilter, does not build if I try to check for null. It works fine if I write "testString" or anything like that, but not with null.
tl:dr; version: How do I create a filter to check if field is null in MongoDb C# driver?
Note, I checked other answers and they recommend $exists does what I want - it does not, as per mongo docs:
When is true, $exists matches the documents that contain the
field, including documents where the field value is null. If
is false, the query returns only the documents that do not contain the
field.
This actually works as expected with a little modification, which depends on:
BsonNull.Value
See this question for details:
How to create Bson Document with Null value using C# official driver?
So the query is:
var fieldValueIsNullFilter = Builders<BsonDocument>.Filter.Eq("MyFieldName", BsonNull.Value);
Initially, I didn't realize BsonNull has a Value property.
In addition to the comment above you can write like this if you have some entity (depends on the property type):
public class CustomEntity
{
public string Id { get; set; }
public string StringProperty { get; set; }
public DateTime? DateTimeProperty { get; set; }
}
var filterIfStringPropertyNull = Builders<CustomEntity>.Filter.Eq(o => o.StringProperty, null); // if property is string
var filterIfDatePropertyNull = Builders<CustomEntity>.Filter.Eq(o => o.DateTimeProperty, BsonNull.Value.ToNullableUniversalTime()); // if property is DateTime?
And so on.
It could be easier
I need to add filtering to my API requests that support AutoQuery, so based on this SO answer, used q.And to add conditions. The issue is that one of the POCO properties is a List<string> and it seems doing a simple Contains() won't work. Here's a simple example of what I have:
public class PocoObject
{
public int Id { get; set; }
public List<string> Names { get; set; }
}
My service looks like this:
public object Get(PocoObjects request)
{
var q = AutoQuery.CreateQuery(request, Request.GetRequestParams());
if (someCondition)
{
q.And(x => x.Names.Contains(request.TargetName));
}
return AutoQuery.Execute(request, q);
}
Problem is, I get an error like this:
variable 'x' of type 'TestProject.ServiceModel.Types.PocoObject' referenced from scope '', but it is not defined
If I change the Contains to a simpler equality comparison on another property, the AutoQuery works. Any ideas how to accomplish this?
You can't do a server side SQL query on a blobbed complex type property like List<string>. Any queries need to be applied on the client after the results are returned from the db and its deserialised back into a typed POCO.
I am trying to make a frontend for a MongoDB database using C#.
So far I have managed to get the connection and the insert method to work.
But I'm stuck with the find method as I am new to C# and .net in general.
public void FindDocument(string query) {
BsonDocument QueryDoc = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>(query);
MongoCursor result = Collection.FindAs(QueryDoc);
The last line is giving me a really long error:
the type arguments for the method 'MongoDB.Driver.MongoCollection.FindAs<TDocument>'(MongoDB.Driver.IMongoQuery) can't be inferrred from usage. Try to specify the type arguments explicitly)
I'm completely lost here. If it is necessary, I can post the entire class here. Let me know. I'm using this driver by the way: CSharpDriver-1.10.0 from https://github.com/mongodb/mongo-csharp-driver/releases
FindAs expects you tell what type (class) you're expecting, so you'd have to call something like Collection.FindAs<MyClass>(query).
However, your code seems a little more complex than necessary. It's usually easier to work with your classes directly and use QueryBuilders to create the query (they can also be passed to other methods as IMongoQuery).
class MyClass {
public ObjectId Id { get; set; }
public ObjectId UserId { get; set; }
public string Description { get; set; }
// ...
}
var coll = mongoDb.GetCollection<MyClass>("MyClass");
var result = coll.Find(Query<MyClass>.EQ(p => p.UserId == someId));
// result is now a MongoCursor<MyClass>
Also, please note that a completely new, async-aware version of the C# driver, is already in beta (currently 2.0.0-beta4). The interface has changed completely, so if you're starting now, it's probably easier to (only) learn the new interface.
Use-case:
PHP symfony project which has to communicate with a C# back-end with Mongo. In the PHP front-end it is possible to make a query to get data from Mongo. This query is send via an API (XML). The C# back-end deserializes this XML to get the objects. Then I want to execute an Linq-to-objects query (which is the query send via the API) on a collection in my memory. So I wanted to make my own "LinqBuilder" so I can query the objects and return them to my PHP front-end.
I have the following object:
public class MongoDoc
{
public int Id { get; set; }
public string Kind { get; set; }
public BsonDocument Data { get; set; }
}
Below is an example of what I'm trying to achieve.
var list = source.Where(x => x.Data["Identifier"] == "H7PXXK").ToList(); // source is collection of MongoDoc objects
The code line above is what I want to build with expressions because it has to be dynamic. What I did achieve is to query the "Kind" property of my MongoDoc object as follow:
ParameterExpression _expr = Expression.Parameter(typeof(MongoDoc), "x");
expression = Expression.Equal(
Expression.PropertyOrField(_expr, "Kind"),
Expression.Constant("KindValue")
);
This will produce the following lamdba:
x => (x.Kind == "KindValue")
That is correct, but now I need to get the property Identifier in the BsonDocument property Data. Normally it would be something like above: x => x.Data["Identifier"] == "Value". This is exactly what my problem is. How can I achieve this?
think it should be something like that.
var _expr = Expression.Parameter(typeof(MongoDoc), "x");
//x.Data
Expression member = Expression.PropertyOrField(_expr, "Data");
//x.Data["Identifier"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("Identifier")});
//x.Data["Identifier"] == "H7PXXK"
member = Expression.Equal(member, Expression.Constant((BsonValue)"H7PXXK"));
EDIT :
from your comment, it should be
//x.Data["MoreData"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("MoreData")});
//x.Data["MoreData"]["Identifier"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("Identifier")});