newbie to CosmoDB how to query collection with multiple values? - c#

I have the following collection and I want to query based on Class and FullName from Students
{
"id" : "ABCD",
"Class" : "Math",
"Students" : [
{
"FullName" : "Dan Smith",
},
{
"FullName" : "Dave Jackson",
},
]
}
The following filter works based on class.
var filter = builder.Eq(x => x.Class, "Math");
var document = collection.Find(filter).FirstOrDefaultAsync();
But I want to query based on student also, I tried to add another filter and it has "Cannot implicitly convert type string to bool" error
filter &= builder.Eq(x => x.Students.Any(y => y.FullName,"Dan"));

As you want to query with the nested document in an array, you need $elemMatch operator. In MongoDB .NET Driver syntax, you can achieve with either of these ways:
Solution 1: ElemMatch with LINQ Expression
filter &= builder.ElemMatch(x => x.Students, y => y.FullName == "Dan");
Solution 2: ElemMatch with FilterDefinition
filter &= builder.ElemMatch(x => x.Students,
Builders<Student>.Filter.Eq(y => y.FullName, "Dan"));
The above methods will return no document as the filter criteria don't match with the attached document.
If you look for matching the partial word, you need to work with $regex operator.
Solution: With regex match
filter &= builder.ElemMatch(x => x.Students,
Builders<Student>.Filter.Regex(y => y.FullName, "Dan"));
Demo

Related

Problem returning array of strings instead of BsonDocument Mongodb c# driver Unwind

I have got a class called Contact which has a field named Numbers and Numbers is a list of strings. I want to return a list containing all numbers in matched documents only. but it gives me an array of BsonDocuments. I need Just a List of numbers.
here is my query:
var query = await _context.ContactLists.Aggregate(new AggregateOptions { AllowDiskUse = true })
.Match(x => x.Id == id && x.CreatorId == user.GetUserId())
.Unwind(x => x.Numbers)
.Project(Builders<BsonDocument>.Projection.Include("Numbers").Exclude("_id"))
.ToListAsync();
it resturns :
[{{ "Numbers" : "989309910790" }}]
and I need :
["989309910790"]
I am not allowed to use Linq driver.

C# Mongo Builders<T>.Filter lambda for an array field

How do I use lambda expressions in C# with Builders and lambda expressions to match against a field in an array of a class. Let me clarify.
Presently I have a document in mongo, and a C# class. I am using aggregation pipeline to match documents. I do this using a lambda expression for type safety and elegance reasons.
I have the follow code to add a filter.
public RegularExpressionFilterSingle<T> AddFilter(
Expression<Func<T, object>> FilterSpecification)
{
FilterDefinitions.Add(
Builders<T>.Filter.
Regex(
FilterSpecification, // e.g. x => x.Email
new BsonRegularExpression(Text,"i")));
return this;
}
The the following code will add a number of filters.
RegularExpressionFilterSingle<User>
RegExp = new RegularExpressionFilterSingle<User>
(new List<FilterDefinition<User>>(), Word.Trim()).
AddFilter(x => x.Email).
AddFilter(x => x.Title).
AddFilter(x => x.FirstName).
AddFilter(x => x.LastName);
This all works nicely the document structure at this stage is flat only fields. Now I have added a field which is list of a class, in C# this is.
[BsonElement("product")]
public List<UserProduct> Products { get; set; } = default;
So I want to add a filter which also matches against items in this array. My partially working attempt is as follows.
RegularExpressionFilterSingle<User>
RegExp = new RegularExpressionFilterSingle<User>
(new List<FilterDefinition<User>>(), Word.Trim()).
AddFilter(x => x.Email).
AddFilter(x => x.Title).
AddFilter(x => x.FirstName).
AddFilter(x => x.LastName).
AddFilter(x => x.Products[1].Name);
You will notice the addition of
AddFilter(x => x.Products[1].Name);
How do I search so that if any of the Products[...].Name matches not just the one at index 1?
I did come across this post (Lambda Expression to filter a list of list of items), but it didn't seem to answer my questions.
Many thanks
the mongo driver can't translate the member expression you need for the regex filter to work in a strongly-typed manner.
you'd have to create your own function that will take in a lambda and convert it to a dotted string path.
i've already created such a function for my mongodb library which you can find here
with it you can simply do the following:
var filter = Builders<User>.Filter
.Regex(Prop.Path<User>(u => u.Products[0].Name),
"^something");
which will translate to the following filter if you do an aggregate query with it:
{
"$match" : {
"Products.Name" : /^something/
}
}

.NET MongoDB Bad Projection Specification

I'm in the process of upgrading a system from the Legacy Mongo Drivers to the new ones. I've got an issue with the following query.
var orgsUnitReadModels = _readModelService.Queryable<OrganisationalUnitReadModel>()
.Where(x => locations.Contains(x.Id))
.Select(x => new AuditLocationItemViewModel
{
Id = x.Id,
Name = x.Name,
AuditRecordId = auditRecordId,
Type = type,
IsArchived = !x.IsVisible,
AuditStatus = auditStatus
}).ToList();
It produces the following error message, which I don't understand. I would be grateful for assistance explaining what this means and how to fix it.
MongoDB.Driver.MongoCommandException: 'Command aggregate failed: Bad
projection specification, cannot exclude fields other than '_id' in an
inclusion projection: { Id: "$_id", Name: "$Name", AuditRecordId:
BinData(3, 5797FCCCA90C8644B4CB84FED4236D4B), Type: 0, IsArchived: {
$not: [ "$IsVisible" ] }, AuditStatus: 2, _id: 0 }.'
In this example LINQ's Select statement gets translated into MongoDB's $project. Typically you use 0 (or false) to exclude fields and 1 or true to include fields in a final result set. Of course you can also use the dollar syntax to refer to existing fields which happens for instance for Name.
The problem is that you're also trying to include some in-memory constant values as part of the projection. Unfortunately one of them (type) is equal to 0 which is interpreted as if you would like to exclude a field called Type from the pipeline result.
Due to this ambiguity MongoDB introduced $literal operator and you can try following syntax in Mongo shell:
db.col.aggregate([{ $project: { _id: 0, Id: 1, Name: 1, Type: { $literal: 0 } } }])
It will return 0 as a constant value as you expect. The MongoDB .NET driver documentation mentions literal here but it looks like it only works for strings.
There's a couple of ways you can solve your problem, I think the easier is to run simpler .Select statement first and then use .ToList() to make sure the query is materialized. Once it's done you can run another in-memory .Select() to build your OrganisationalUnitReadModel:
.Where(x => locations.Contains(x.Id))
.Select(x => new { x.Id, x.Name, x.IsVisible }).ToList()
.Select(x => new AuditLocationItemViewModel
{
Id = x.Id,
Name = x.Name,
Type = type,
IsArchived = !x.IsVisible,
AuditStatus = auditStatus
}).ToList();

c# groupBy failing with key not found exception

I retrieve BsonDocuments using the mongoDb driver in C#.
After this, I have a grouping method used to regroup my documents with a given Key.
Here is an example of objects returned from mongo :
{{ "_id" : ObjectId("57762d37de7d9c1cbc53bc10"), "DraftNumber" : "227232AA", "EndDate" : ISODate("2016-09-29T08:45:14.986Z"), "ProjectNumber" : "17E618BB" }}
Here is the beginning of my method used to regroup :
internal List<Project> RegroupProject(IEnumerable<BsonDocument> projects)
{
var regroupProjectList = new List<Project>();
var groupedProject = projects.GroupBy(project => project["ProjectNumber"]).ToList();
[...]
}
And I got the following exception en groupBy expression :
Element 'ProjectNumber' not found.
What do you think about that ?
Is it because one of my 18k element has no value for ProjectNumber field ?
You have guessed correctly that one or more of the 18k items doesn't have a ProjectNumber property. There's a couple of options that I can see.
Filter out the items that don't have the property:
var groupedProject = projects
.Where(p => p.Contains("ProjectNumber"))
.GroupBy(project => project["ProjectNumber"])
.ToList();
Specify a default value for items missing the value:
var groupedProject = projects
.GroupBy(project => project["ProjectNumber", "<DefaultValue>"])
.ToList();

DocumentDB LINQ match multiple child objects

I have a simple document.
{
Name: "Foo",
Tags: [
{ Name: "Type", Value: "One" },
{ Name: "Category", Value: "A" },
{ Name: "Source", Value: "Example" },
]
}
I would like to make a LINQ query that can find these documents by matching multiple Tags.
i.e. Not a SQL query, unless there is no other option.
e.g.
var tagsToMatch = new List<Tag>()
{
new Tag("Type", "One"),
new Tag("Category", "A")
};
var query = client
.CreateDocumentQuery<T>(documentCollectionUri)
.Where(d => tagsToMatch.All(tagToMatch => d.Tags.Any(tag => tag == tagToMatch)));
Which gives me the error Method 'All' is not supported..
I have found examples where a single property on the child object is being matched: LINQ Query Issue with using Any on DocumentDB for child collection
var singleTagToMatch = tagsToMatch.First();
var query = client
.CreateDocumentQuery<T>(documentCollectionUri)
.SelectMany
(
d => d.Tags
.Where(t => t.Name == singleTagToMatch.Name && t.Value == singleTagToMatch.Value)
.Select(t => d)
);
But it's not obvious how that approach can be extended to support matching multiple child objects.
I found there's a function called ARRAY_CONTAINS which can be used: Azure DocumentDB ARRAY_CONTAINS on nested documents
But all the examples I came across are using SQL queries.
This thread indicates that LINQ support was "coming soon" in 2015, but it was never followed up so I assume it wasn't added.
I haven't come across any documentation for ARRAY_CONTAINS in LINQ, only in SQL.
I tried the following SQL query to see if it does what I want, and it didn't return any results:
SELECT Document
FROM Document
WHERE ARRAY_CONTAINS(Document.Tags, { Name: "Type", Value: "One" })
AND ARRAY_CONTAINS(Document.Tags, { Name: "Category", Value: "A" })
According to the comments on this answer, ARRAY_CONTAINS only works on arrays of primitives, not objects. SO it appears not to be suited for what I want to achieve.
It seems the comments on that answer are wrong, and I had syntax errors in my query. I needed to add double quotes around the property names.
Running this query did return the results I wanted:
SELECT Document
FROM Document
WHERE ARRAY_CONTAINS(Document.Tags, { "Name": "Type", "Value": "One" })
AND ARRAY_CONTAINS(Document.Tags, { "Name": "Category", "Value": "A" })
So ARRAY_CONTAINS does appear to achieve what I want, so I'm looking for how to use it via the LINQ syntax.
Using .Contains in the LINQ query will generate SQL that uses ARRAY_CONTAINS.
So:
var tagsToMatch = new List<Tag>()
{
new Tag("Type", "One"),
new Tag("Category", "A")
};
var singleTagToMatch = tagsToMatch.First();
var query = client
.CreateDocumentQuery<T>(documentCollectionUri)
.Where(d => d.Tags.Contains(singleTagToMatch));
Will become:
SELECT * FROM root WHERE ARRAY_CONTAINS(root["Tags"], {"Name":"Type","Value":"One"})
You can chain .Where calls to create a chain of AND predicates.
So:
var query = client.CreateDocumentQuery<T>(documentCollectionUri)
foreach (var tagToMatch in tagsToMatch)
{
query = query.Where(s => s.Tags.Contains(tagToMatch));
}
Will become:
SELECT * FROM root WHERE ARRAY_CONTAINS(root["Tags"], {"Name":"Type","Value":"One"}) AND ARRAY_CONTAINS(root["Tags"], {"Name":"Category","Value":"A"})
If you need to chain the predicates using OR then you'll need some expression predicate builder library.

Categories