MongoDB C# driver, query by an array element using regex - c#

I am using MongoDB C# driver version 2.2. My collection contains "Parent" objects. Each parent object has an array of children objects. Each child has name value:
"parent": {
"children":[
{ "name": "Bob", "age": 10},
{ "name": "Alice", "age": 7},
{ "name": "Tobias", "age": 11}
]
}
I need to translate the following code into C# statements / LINQ syntax:
db.getCollection('Parents').find({'parent.children': { $elemMatch: { 'name': { $regex: '.*ob.*', $options: 'im' } }}})
I have found there are methods like
var builder = Builders<BsonDocument>.Filter;
builder.Regex("parent.children.name", new BsonRegularExpression(".*ob.*")); //does not work with array
and
builder.AnyEq("parent.children.name", "ob"); //without regex
But I cannot understand how to combine them. Please advise.
UPDATE:
I am using the following for now, please correct me if you know a reason why it should not work correctly:
builder.AnyEq("parent.children.name", new BsonRegularExpression(".*ob.*"))

I am using the following for now:
builder.AnyEq("parent.children.name", new BsonRegularExpression(".*ob.*"))

Can't test C# on this machine. Let me know if this doesn't work:
var filter = Builders<People>.Filter.ElemMatch(x => x.Parent.Children, x => Regex.IsMatch(x.Name, "regex"));
var res = await collection.Find(filter).ToListAsync();
Here's a trick you might like btw:
// Take your inputted `find` query string:
string bsonQuery = "{'parent.children': { $elemMatch: { 'name': { $regex: '.*ob.*', $options: 'im' } }}}";
// Use it as the filter!
var filter = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>(bsonQuery);
// Results:
var result = col.FindSync (filter).ToList();

I've tested your current expression (builder.AnyEq("parent.children.name", new BsonRegularExpression(".*ob.*")) on a database at home and I don't believe it behaves in the way you intend.
Although the c# documentation doesn't explicitly state this, mongoDB supports the Regex filter on an array field. I have tested the below expression in C# and have correct results for the Regex despite the field being an array.
builder.Regex(MONGO_FIELD_NAME, new BsonRegularExpression("SOME REGEX"));
In addition, I've tested Regex on the toy example in the [online mongo webshell for query arrays] (https://docs.mongodb.com/manual/tutorial/query-arrays/). Querying for db.inventory.find({tags : {$regex : "^bl"}} will return results with "blank" or "blue" despite the "tag" field being an array.

Related

MongoDB C# search array of objects for intersection with a list on a single field

I'm trying to search a embedded array of documents in my MongoDB documents for a match in a list of ObjectIds I have in a C# list.
For example If I have a class object in mongodb looking like this
{
"_id": $oid,
"Name":"Politics 101",
Students: [{ "_id": 'theirid', "Name": "Ghengis Khan", "StudentId": 12345 }, ... ]
}
How can I query that list towards all classes containing a student with a student ID I have in a list with C#?
If the embedded list was just an array of studentIds I think I could just do something like this
List<int> studentIds = new List<int>() { 12345,123213,434233,234232,42312,776433 }
FilterDefinition<Class> filter = Builders<Class>.Filter.AnyIn(c => c.Students, studentIds);
var desiredClasses = database.Classes.Class.Find(filter).ToListAsync()
But what do I do when I need to match a field in an array of embedded documents?
Solution 1: Work with .ElemMatch()
MongoDB query
db.collection.find({
"Students": {
$elemMatch: {
"StudentId": {
$in: [
12345,
123213,
434233,
234232,
42312,
776433
]
}
}
}
})
Demo Solution 1 # Mongo Playground
MongoDB .NET Driver syntax
FilterDefinition<Class> filter = Builders<Class>.Filter.ElemMatch(c => c.Students,
Builders<Student>.Filter.In(s => s.StudentId, studentIds));
Demo
Solution 2: Work with .In() and dot notation
MongoDB query
db.collection.find({
"Students.StudentId": {
$in: [
12345,
123213,
434233,
234232,
42312,
776433
]
}
})
Demo Solution 2 # Mongo Playground
MongoDB .NET Driver syntax
FilterDefinition<Class> filter = Builders<Class>.Filter
.In($"{nameof(Class.Students)}.{nameof(Student.StudentId)}", studentIds);
Demo

C# MongoDB Driver: Can't find the way to run complex query for AnyIn filter in MongoDB

I have a document like this:
{
"id": "xxxxxxxxxxxx",
"groupsAuthorized": [
"USA/California/SF",
"France/IDF/Paris"
]
}
And I have an user that has a list of authorized groups, like for example the following:
"groups": [
"France/IDF",
"USA/NY/NYC"
]
What I'm trying to achieve is to retrieve all documents in the database that the user is authorized to retrieve, essentially I want to be able to check in the list "groupsAuthorized" if one of the group contains a subset of an element of the other list "groups" contained in my user authorizations
using the following values:
my document:
{
"id": "xxxxxxxxxxxx",
"groupsAuthorized": [
"USA/California/SF",
"France/IDF/Paris"
]
}
my user permissions:
"groups": [
"France/IDF",
"USA/NY/NYC"
]
the user should be able to retrieve this document as the string "France/IDF" is correctly contained in the string "France/IDF/Paris", however, if the values would've been like this:
my document:
{
"id": "xxxxxxxxxxxx",
"groupsAuthorized": [
"USA/California/SF",
"France/IDF"
]
}
my user permissions:
"groups": [
"France/IDF/Paris",
"USA/NY/NYC"
]
it should not work, because my user is only authorized to view documents from France/IDF/Paris and USA/NY/NYC and none of the string inside of the authorizedGroups of my document contains those sequences
I've tried to use a standard LINQ query to achieve this which is fairly simple:
var userAuthorizedGroups = new List<string> { "France/IDF/Paris", "USA/NY/NYC" };
var results = collection.AsQueryable()
.Where(entity => userAuthorizedGroups
.Any(userGroup => entity.authorizedGroups
.Any(entityAuthorizedGroup => entityAuthorizedGroup.Contains(userGroup))));
But i'm getting the famous unsupported filter exception that it seems lot of people is having, i've tried different options found on the internet like the following:
var userAuthorizedGroups = new List<string> { "France/IDF/Paris", "USA/NY/NYC" };
var filter = Builders<PartitionedEntity<Passport>>.Filter.AnyIn(i => i.authorizedGroups, userAuthorizedGroups);
var results = (await collection.FindAsync(filter)).ToList();
return results;
But the problem is this will only check if one of the element of the array is contained inside the other array, It will not correctly work for case like "France/IDF" that should correctly match "France/IDF/Paris" because "France/IDF" string is contained inside the "France/IDF/Paris" string inside of my document
I'm getting a bit clueless on how to achieve this using the mongodb C# driver, i'm starting to think that I should just pull all documents to client and do the filtering manually but that would be quite messy
Has anyone an Idea on this subject ?
i'm starting to think that I should just pull all documents to client and do the filtering manually but that would be quite messy
don't do it :)
One place you can start with is here. It describes all the LINQ operators that are supported by the MongoDB .NET driver. As you can see .Contains() isn't mentioned there which means you can't use it and you'll get an arror in the runtime but it does not mean that there's no way to do what you're trying to achieve.
The operator closest to contains you can use is $indexOfBytes which returns -1 if there's no match and the position of a substring otherwise. Also since you need to match an array against another array you need two pairs of $map and $anyElementTrue to do exactly what .NET's .Any does.
Your query (MongoDB client) can look like this:
db.collection.find({
$expr: {
$anyElementTrue: {
$map: {
input: "$groupsAuthorized",
as: "group",
in: {
$anyElementTrue: {
$map: {
input: ["France/IDF/Paris", "USA/NY/NYC"],
as: "userGroup",
in: { $ne: [ -1, { $indexOfBytes: [ "$$userGroup", "$$group" ] } ] }
}
}
}
}
}
}
})
Mongo Playground,
You can run the same query from .NET using BsonDocument class which takes a string (JSON) and converts into a query:
var query = BsonDocument.Parse(#"{
$expr: {
$anyElementTrue:
{
$map:
{
input: '$groupsAuthorized',
as: 'group',
in: {
$anyElementTrue:
{
$map:
{
input: ['France/IDF/Paris', 'USA/NY/NYC'],
as: 'userGroup',
in: { $ne: [-1, { $indexOfBytes: ['$$userGroup', '$$group'] } ] }
}
}
}
}
}
}
}");
var result = col.Find(query).ToList();

Mongodb Array Update

I have following mongodb json.I would like to update price of product_id:2 from 4 to 5 in the document with document _id:id1
Can someone guide me how to do that in mongodb or using c#
{
"_id": "id1",
"products": [
{
"product_id": "pr1",
"price":1,
"qty":5
},
{
"product_id": "pr2",
"price":4,
"qty":10
},
{
"product_id": "pr3",
"price":8,
"qty":9
}
]
}
Download the C# mongodb driver from mondodb site.
var collection = DataBase.GetCollection<MyProducts>("products");
var query = Query<MyProduct>.Where(p => (m.productID == "pr2"));
var product = collection.FindOne(query);
product.Price = 5;
collection.Save(product);
Either use the .update function in mongo db or pull the object out using linq something like:
var query = (from c in Collection.AsQueryable<T>() where c.Id == Id select c).First<T>();
adjusting the above variables accordingly.
Then changing whatever that object is then saving it using Collection.Save(obj) this will re write the whole object. Look here: http://docs.mongodb.org/manual/reference/method/db.collection.save/ and http://docs.mongodb.org/manual/tutorial/modify-documents/

Using filters with JsonPath in C#

I'm using JsonPath for C# to query some JSON data. JsonPath doesn't come with its own parser, so as per Rick Sladkey's advice, I'm using Json.NET to parse my Json string into a collection of nested IDictionary objects, IList arrays, and primitives. Then I use JsonPath to filter it (after adding the class suggested in Rick Sladkey's answer).
For reference, here's the part of my code that actually handles all this:
// string exampleJsonString defined below
string query = "$.store.book[*].title" // we should get a list of all titles
// step 1: use Json.NET to parse the json string into a JObject
JObject parsedJson = JObject.Parse(exampleJsonString);
// step 2: use JsonPath with a custom ValueSystem (JsonNetValueSystem)
JsonPathContext context = new JsonPathContext
{ ValueSystem = new JsonNetValueSystem() };
// step 3: get results using JsonPath
List<object> toRet = context.SelectNodes(parsedJson,
query).Select(node => node.Value).ToList();
The reason I was using JsonPath in the first place was because of its filter functionality. You can not only do normal queries like "$.store.book[*].title" (to get an array of all the titles in the book store), but also queries like "$.store.book[?(#.category == 'fiction')].title" (to get an array of all titles in the book store whose category matches 'fiction'). I need to be able to pass entire queries as a string, and so being able to filter while querying is extremely helpful.
Unfortunately, I'm having some trouble getting this filter functionality to work. I expect that I'll have to make adjustments to either the JsonNetValueSystem class (defined originally in the aforementioned stack overflow answer) or the JsonPath namespace (you can get JsonPath.cs from JsonPath's google code page). If there's some external tool or an alternative parsing mechanism to Json.NET that would allow me to keep JsonPath's filtering without having to write too much extra code, that would be ideal, but I'm pretty sure I'll have to alter either JsonNetValueSystem or JsonPath itself. (Both of these are fairly easy to alter since they're just .cs files, but I can't really dig into Json.NET without a lot more work.)
I can't actually seem to figure out where the original JsonPath code handles filtering, nor can I figure out why the JsonNetValueSystem class robs it of that functionality. Any advice for how to add the ability to filter in the query string would be very much appreciated. Even if it's just "don't mess with JsonPath, just change JsonNetValueSystem" or vice versa.
string exampleJsonString = "{
"store": {
"book": [ {
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}, {
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}, {
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}, {
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
} ],
"bicycle": [ {
"color": "red",
"price": 19.95,
"style": [ "city", "hybrid" ]
}, {
"color": "blue",
"price": 59.91,
"style": [ "downhill", "freeride" ]
} ]
}
}"
When you use the script expression in a query (the ?(...) part), you need to provide a ScriptEvaluator method to evaluate the script. Unfortunately they don't provide a default implementation for the C# version. You'll need to provide that implementation.
Out of the box, this won't be the most trivial problem to solve, you'll need to write an interpreter. You have a couple of options: use CodeDOM to compile and execute the script as C# code (or any language you would prefer), use Roslyn to parse the script and evaluate, whatever works.
A quick and dirty solution for this particular case would be to do something like this in your script evaluator method:
object EvaluateScript(string script, object value, string context)
{
if (script == "#.category == 'fiction'")
{
var obj = value as JObject;
return (string)obj["category"] == "fiction";
}
return null;
}
Here's another solution which utilizes IronPython to evaluate the script.
public class IronPythonScriptEvaluator
{
private Lazy<ScriptEngine> engine = new Lazy<ScriptEngine>(() => Python.CreateEngine());
private ScriptEngine Engine { get { return engine.Value; } }
private const string ItemName = "_IronPythonScriptEvaluator_item";
public object EvaluateScript(string script, object value, string context)
{
var cleanScript = CleanupScript(script);
var scope = Engine.CreateScope();
scope.SetVariable(ItemName, value);
return Engine.Execute<bool>(cleanScript, scope);
}
private string CleanupScript(string script)
{
return Regex.Replace(script, #"#", ItemName);
}
}
Another solution is to use Manatee.Json instead. It has a native JSONPath implementation and parser all built in (along with schema and a serializer). Moreover, you aren't required to represent the path as a string. Manatee.Json has a fluent interface that you can use to build paths, including expression support.
To represent $.store.book[*].title, you would have
var path = JsonPathWith.Name("store")
.Name("book")
.Array()
.Name("title");
For your example $.store.book[?(#.category == 'fiction')].title, you'd use
var path = JsonPathWith.Name("store")
.Name("book")
.Array(jv => jv.Name("category") == "fiction")
.Name("title");
What's more is that there is limited support for fields within those expressions as well.
If your path source is a string, Manatee.Json handles path parsing, too!

How to query indexes in mongodb using the C# driver

I have the following data structure:
{
eventname: "blah",
invitees: [
{
inviteid: 1,
userid: 34234
},
{
inviteid: 2,
userid: 5232
}]
}
I am going to use ensureIndex on my invitees column so i do not have to search through every document to find specific userids in the invitees column. Its basically searching for events that a specific userid was invited to. I was suggested to use this db.events.find({"invitees.userid" : 34234}) to query it, but how do i do this in c# with then 10gen driver. the .find method only accepts a Mongo Query object.
The way that I'm doing it is:
var collection = db.GetCollection<MyType>("collectionName");
var query = Query.EQ("fieldname", valueToQuery);
var results = collection.Find(query);

Categories