I have this query which works when I run it in Robo 3T:
db.getCollection('groupSchedule').aggregate([{ "$match" : { "GroupId" : ObjectId("598dd346e5549706a80680bf") } },
{ "$lookup" : { "from" : "schedule", "localField" : "ScheduleIds", "foreignField" : "_id", "as" : "Schedule" } },
{ "$unwind" : "$Schedule" },
{ "$match" : {"$or": [{ "Schedule.End.ByDate" : {"$gte":new Date()}},{ "Schedule.End.ByDate" : null}] } },
{ "$group" : { "_id" : "$GroupId", "SurveyIds" : { "$addToSet" : "$Schedule.SurveyId" }, "ScheduleIds" : { "$addToSet" : "$Schedule._id" } } },
{ "$project" : { "_id" : 0, "SurveyIds" : 1, "ScheduleIds": 1 } }])
However, when I try to do the same thing using the C# driver as it blows up saying that:
"Duplicate element name 'Schedule.End.ByDate'.",
Here's the code:
return new List<BsonDocument>
{
Common.Util.MongoUtils.Match(new BsonDocument { { "GroupId", groupId } }),
Common.Util.MongoUtils.Lookup(scheduleCollections, "ScheduleIds", "_id", "Schedule"),
Common.Util.MongoUtils.Unwind("$Schedule"),
Common.Util.MongoUtils.Match(new BsonDocument
{
{
"$or", new BsonDocument
{
{
"Schedule.End.ByDate", BsonNull.Value
},
{
"Schedule.End.ByDate", new BsonDocument
{
{
"$gte", DateTime.UtcNow
}
}
}
}
}
}),
Group(),
Common.Util.MongoUtils.Project(new BsonDocument
{
{ "_id", 0 },
{ "SurveyIds", 1 },
{ "Schedules", 1 }
})
};
Any thoughts?
By using BsonDocument for your $or operator, you're effectively trying to create the following:
"$or": {
"Schedule.End.ByDate": null,
"Schedule.End.ByDate": { "$gte" : ISODate("...") }
}
If we look again at your error message:
"Duplicate element name 'Schedule.End.ByDate'.",
It's clear that you have duplicated the Schedule.End.ByDate element name, which is invalid and not what was intended.
Instead, you want to use BsonArray to wrap two separate objects, in order to produce the result you have in your Robo 3T query. To achieve that, you can use the following adaptation of your C# code:
"$or", new BsonArray
{
new BsonDocument
{
{ "Schedule.End.ByDate", BsonNull.Value }
},
new BsonDocument
{
{
"Schedule.End.ByDate", new BsonDocument
{
{ "$gte", DateTime.UtcNow }
}
}
}
}
This produces the following, which matches your Robo 3T query for the $or section:
{ "$or" : [
{ "Schedule.End.ByDate" : null },
{ "Schedule.End.ByDate" : { "$gte" : ISODate("...") } }
] }
Related
I have two collections one is for posts (PostInfo) and one for users (UserInfo), I join two collections and I want to find the posts if the given userid is in AsUser.Friends :
var docs = await _dbContext.PostInfos.Aggregate()
.Lookup("UserInfo", "UserId", "UserId", "AsUser")
.Unwind("AsUser")
.Match(
new BsonDocument() {
{ "$expr", new BsonDocument() {
{ "$in", new BsonArray(){ "$AsUser.Friends", BsonArray.Create(user.UserId) } }
}
}
}
)
.As<PostInfo>()
.Project<PostInfo>(Builders<PostInfo>.Projection.Exclude("AsUser"))
.ToListAsync();
This is userinfo document :
{
"_id" : ObjectId("62d64398772c29b212332ec2"),
"UserId" : "18F1FDB9-E5DE-4116-9486-271FE6738785",
"IsDeleted" : false,
"UserName" : "kaveh",
"Followers" : [],
"Followings" : [],
"Friends" : [
"9e3163b9-1ae6-4652-9dc6-7898ab7b7a00",
"2B5F6867-E804-48AF-BED3-672EBD770D10"
],
}
I am having a problem working with the $in operator.
Update
Also, I think this would work too (from here):
db.inventory.find( { tags: { $eq: [ "A", "B" ] } } )
But I can't convert this to C# format.
The $in operator (logic) is incorrect, you should check whether the userId in the AsUser.Friends array as below:
{
$match: {
$expr: {
$in: [
"9e3163b9-1ae6-4652-9dc6-7898ab7b7a00", // UserId
"$AsUser.Friends"
]
}
}
}
Sample Mongo Playground
For MongoDB C# syntax,
.Match(
new BsonDocument()
{
{
"$expr", new BsonDocument()
{
{ "$in", new BsonArray() { user.UserId, "$AsUser.Friends" } }
}
}
}
)
I'm having difficulties putting up a code which returns an element in an array of subdocuments. I am actually trying to flatten a document to a new document which is strongly typed. My document is looking like;
{
"_id" : BinData(3, "7FRf4nbe60ev6XmGKBBW4Q=="),
"status" : NumberInt(1),
"title":"Central station",
"attributes" : [
{
"defId" : BinData(3, "QFDtR03NbkqwuhhG76wS8g=="),
"value" : "388",
"name" : null
},
{
"defId" : BinData(3, "RE3MT3clb0OdLEkkqhpFOg=="),
"value" : "",
"name" : null
},
{
"defId" : BinData(3, "pPgJR50h8kGdDaCcH2o17Q=="),
"value" : "Merkez",
"name" : null
}
]}
What I am trying to achieve is;
{
"title":"Central Station",
"value":"388"
}
What I've done already;
using (_dbContext)
{
var filter = Builders<CustomerModel>.Filter.Eq(q => q.Id, Guid.Parse("30B59585-CBFC-4CD5-A43E-0FDB0AE3167A")) &
Builders<CustomerModel>.Filter.ElemMatch(f => f.Attributes, q => q.DefId == Guid.Parse("47ED5040-CD4D-4A6E-B0BA-1846EFAC12F2"));
var projection = Builders<CustomerModel>.Projection.Include(f => f.Title).Include("attributes.value");
var document = _dbContext.Collection<CustomerModel>().Find(filter).Project(projection).FirstOrDefault();
if (document == null)
return null;
return BsonSerializer.Deserialize<TitleAndValueViewModel>(document);
}
Note: TitleAndCodeViewModel contains title and value properties.
This block of code returns;
{{ "_id" : CSUUID("30b59585-cbfc-4cd5-a43e-0fdb0ae3167a"), "title" : "388 güvenevler", "attributes" : [{ "value" : "388" }, { "value" : "" }, { "value" : "Merkez " }] }}
I am trying to get "value":"388" but instead I am getting another two value properties even tough the ElemMatch filter added for subdocument.
Thank you for your help in advance.
Note: I am looking for answers in C# mongodb driver.
Option 1: ( via aggregation)
db.collection.aggregate([
{
$match: {
_id: 5,
"attributes.defId": 1
}
},
{
"$addFields": {
"attributes": {
"$filter": {
"input": "$attributes",
"as": "a",
"cond": {
$eq: [
"$$a.defId",
1
]
}
}
}
}
},
{
$unwind: "$attributes"
},
{
$project: {
_id: 0,
title: 1,
value: "$attributes.value"
}
}
])
Explained:
Match ( good to add index for the matching fields )
Filter only the attribute you need
Unwind to convert the array to object
Project only the necessary output
Playground
Option 2: ( find/$elemMatch )
db.collection.find({
_id: 5,
attributes: {
"$elemMatch": {
"defId": 1
}
}
},
{
_id: 0,
title: 1,
"attributes": {
"$elemMatch": {
"defId": 1
}
}
})
Explained:
Match the element via _id and elemMatch the attribute
Project the necessary elements. ( Note here elemMatch also need to be used to filter the exact match attribute )
( Note this version will not identify if there is second attribute with same attribute.defId , also projection of attribute will be array with single element if found that need to be considered from the app side )
Playground 2
by specifying defId
db.collection.aggregate(
[{
$project: {
title: '$title',
attributes: {
$filter: {
input: '$attributes',
as: 'element',
cond: { $eq: ['$$element.defId', BinData(3, 'QFDtR03NbkqwuhhG76wS8g==')] }
}
}
}
}, {
$project: {
_id: 0,
title: '$title',
value: { $first: '$attributes.value' }
}
}])
result:
{
"title": "Central station",
"value": "388"
}
I have the following MongoDb query working:
db.Entity.aggregate(
[
{
"$match":{"Id": "12345"}
},
{
"$lookup": {
"from": "OtherCollection",
"localField": "otherCollectionId",
"foreignField": "Id",
"as": "ent"
}
},
{
"$project": {
"Name": 1,
"Date": 1,
"OtherObject": { "$arrayElemAt": [ "$ent", 0 ] }
}
},
{
"$sort": {
"OtherObject.Profile.Name": 1
}
}
]
)
This retrieves a list of objects joined with a matching object from another collection.
Does anybody know how I can use this in C# using either LINQ or by using this exact string?
I tried using the following code but it can't seem to find the types for QueryDocument and MongoCursor - I think they've been deprecated?
BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);
There is no need to parse the JSON. Everything here can actually be done directly with either LINQ or the Aggregate Fluent interfaces.
Just using some demonstration classes because the question does not really give much to go on.
Setup
Basically we have two collections here, being
entities
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
and others
{
"_id" : ObjectId("5b08cef10a8a7614c70a5712"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
"name" : "Sub-A"
}
{
"_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
"name" : "Sub-B"
}
And a couple of classes to bind them to, just as very basic examples:
public class Entity
{
public ObjectId id;
public string name { get; set; }
}
public class Other
{
public ObjectId id;
public ObjectId entity { get; set; }
public string name { get; set; }
}
public class EntityWithOthers
{
public ObjectId id;
public string name { get; set; }
public IEnumerable<Other> others;
}
public class EntityWithOther
{
public ObjectId id;
public string name { get; set; }
public Other others;
}
Queries
Fluent Interface
var listNames = new[] { "A", "B" };
var query = entities.Aggregate()
.Match(p => listNames.Contains(p.name))
.Lookup(
foreignCollection: others,
localField: e => e.id,
foreignField: f => f.entity,
#as: (EntityWithOthers eo) => eo.others
)
.Project(p => new { p.id, p.name, other = p.others.First() } )
.Sort(new BsonDocument("other.name",-1))
.ToList();
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"localField" : "_id",
"foreignField" : "entity",
"as" : "others"
} },
{ "$project" : {
"id" : "$_id",
"name" : "$name",
"other" : { "$arrayElemAt" : [ "$others", 0 ] },
"_id" : 0
} },
{ "$sort" : { "other.name" : -1 } }
]
Probably the easiest to understand since the fluent interface is basically the same as the general BSON structure. The $lookup stage has all the same arguments and the $arrayElemAt is represented with First(). For the $sort you can simply supply a BSON document or other valid expression.
An alternate is the newer expressive form of $lookup with a sub-pipeline statement for MongoDB 3.6 and above.
BsonArray subpipeline = new BsonArray();
subpipeline.Add(
new BsonDocument("$match",new BsonDocument(
"$expr", new BsonDocument(
"$eq", new BsonArray { "$$entity", "$entity" }
)
))
);
var lookup = new BsonDocument("$lookup",
new BsonDocument("from", "others")
.Add("let", new BsonDocument("entity", "$_id"))
.Add("pipeline", subpipeline)
.Add("as","others")
);
var query = entities.Aggregate()
.Match(p => listNames.Contains(p.name))
.AppendStage<EntityWithOthers>(lookup)
.Unwind<EntityWithOthers, EntityWithOther>(p => p.others)
.SortByDescending(p => p.others.name)
.ToList();
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"let" : { "entity" : "$_id" },
"pipeline" : [
{ "$match" : { "$expr" : { "$eq" : [ "$$entity", "$entity" ] } } }
],
"as" : "others"
} },
{ "$unwind" : "$others" },
{ "$sort" : { "others.name" : -1 } }
]
The Fluent "Builder" does not support the syntax directly yet, nor do LINQ Expressions support the $expr operator, however you can still construct using BsonDocument and BsonArray or other valid expressions. Here we also "type" the $unwind result in order to apply a $sort using an expression rather than a BsonDocument as shown earlier.
Aside from other uses, a primary task of a "sub-pipeline" is to reduce the documents returned in the target array of $lookup. Also the $unwind here serves a purpose of actually being "merged" into the $lookup statement on server execution, so this is typically more efficient than just grabbing the first element of the resulting array.
Queryable GroupJoin
var query = entities.AsQueryable()
.Where(p => listNames.Contains(p.name))
.GroupJoin(
others.AsQueryable(),
p => p.id,
o => o.entity,
(p, o) => new { p.id, p.name, other = o.First() }
)
.OrderByDescending(p => p.other.name);
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"localField" : "_id",
"foreignField" : "entity",
"as" : "o"
} },
{ "$project" : {
"id" : "$_id",
"name" : "$name",
"other" : { "$arrayElemAt" : [ "$o", 0 ] },
"_id" : 0
} },
{ "$sort" : { "other.name" : -1 } }
]
This is almost identical but just using the different interface and produces a slightly different BSON statement, and really only because of the simplified naming in the functional statements. This does bring up the other possibility of simply using an $unwind as produced from a SelectMany():
var query = entities.AsQueryable()
.Where(p => listNames.Contains(p.name))
.GroupJoin(
others.AsQueryable(),
p => p.id,
o => o.entity,
(p, o) => new { p.id, p.name, other = o }
)
.SelectMany(p => p.other, (p, other) => new { p.id, p.name, other })
.OrderByDescending(p => p.other.name);
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"localField" : "_id",
"foreignField" : "entity",
"as" : "o"
}},
{ "$project" : {
"id" : "$_id",
"name" : "$name",
"other" : "$o",
"_id" : 0
} },
{ "$unwind" : "$other" },
{ "$project" : {
"id" : "$id",
"name" : "$name",
"other" : "$other",
"_id" : 0
}},
{ "$sort" : { "other.name" : -1 } }
]
Normally placing an $unwind directly following $lookup is actually an "optimized pattern" for the aggregation framework. However the .NET driver does mess this up in this combination by forcing a $project in between rather than using the implied naming on the "as". If not for that, this is actually better than the $arrayElemAt when you know you have "one" related result. If you want the $unwind "coalescence", then you are better off using the fluent interface, or a different form as demonstrated later.
Querable Natural
var query = from p in entities.AsQueryable()
where listNames.Contains(p.name)
join o in others.AsQueryable() on p.id equals o.entity into joined
select new { p.id, p.name, other = joined.First() }
into p
orderby p.other.name descending
select p;
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"localField" : "_id",
"foreignField" : "entity",
"as" : "joined"
} },
{ "$project" : {
"id" : "$_id",
"name" : "$name",
"other" : { "$arrayElemAt" : [ "$joined", 0 ] },
"_id" : 0
} },
{ "$sort" : { "other.name" : -1 } }
]
All pretty familiar and really just down to functional naming. Just as with using the $unwind option:
var query = from p in entities.AsQueryable()
where listNames.Contains(p.name)
join o in others.AsQueryable() on p.id equals o.entity into joined
from sub_o in joined.DefaultIfEmpty()
select new { p.id, p.name, other = sub_o }
into p
orderby p.other.name descending
select p;
Request sent to server:
[
{ "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
{ "$lookup" : {
"from" : "others",
"localField" : "_id",
"foreignField" : "entity",
"as" : "joined"
} },
{ "$unwind" : {
"path" : "$joined", "preserveNullAndEmptyArrays" : true
} },
{ "$project" : {
"id" : "$_id",
"name" : "$name",
"other" : "$joined",
"_id" : 0
} },
{ "$sort" : { "other.name" : -1 } }
]
Which actually is using the "optimized coalescence" form. The translator still insists on adding a $project since we need the intermediate select in order to make the statement valid.
Summary
So there are quite a few ways to essentially arrive at what is basically the same query statement with exactly the same results. Whilst you "could" parse the JSON to BsonDocument form and feed this to the fluent Aggregate() command, it's generally better to use the natural builders or the LINQ interfaces as they do easily map onto the same statement.
The options with $unwind are largely shown because even with a "singular" match that "coalescence" form is actually far more optimal then using $arrayElemAt to take the "first" array element. This even becomes more important with considerations of things like the BSON Limit where the $lookup target array could cause the parent document to exceed 16MB without further filtering. There is another post here on Aggregate $lookup Total size of documents in matching pipeline exceeds maximum document size where I actually discuss how to avoid that limit being hit by using such options or other Lookup() syntax available to the fluent interface only at this time.
I've been trying to execute a MongoDB graphLookup using latest C# driver which uses AggregateFluentExtensions. According to the documentation:
the method receives a series of parameters that I can't find a way to make work.
Has anybody used it and could help me with an example?
This is the json version of my aggregation:
db.getCollection("Item").aggregate(
[
{
"$project" : {
"itemMasterId" : 1.0,
"parentItemId" : 1.0
}
},
{
"$graphLookup" : {
"from" : "Item",
"startWith" : "$itemMasterId",
"connectFromField" : "itemMasterId",
"connectToField" : "parentItemId",
"as" : "ancestors",
"maxDepth" : 10,
"depthField" : "depthField",
"restrictSearchWithMatch" : {
"locationId" : 26
}
}
},
{
"$project" : {
"itemMasterId" : 1.0,
"parentItemId" : 1.0,
"children.itemMasterId" : 1,
"children.parentItemId" : 1
}
}
]
);
Thanks!
(Late in the party, I hope it helps someone.)
I have used AppendStage() method, you can try this:
var graphLookupStage = new BsonDocument("$graphLookup",
new BsonDocument
{
{ "from", "someCollection" },
{ "startWith", "$reportsTo" },
{ "connectFromField", "reportsTo"},
{ "connectToField", "name" },
{ "as", "reportingHierarchy" },
{ "maxDepth", 1 },
{ "depthField", "depthField" } //optional
});
var result = collection.Aggregate().AppendStage<BsonDocument>(graphLookupStage);
I'm trying the following (simplistic scenario):
I have two documents that look like this (cinema has timeslots, timeslot has a movie):
{
"_id":"5b5dd8932dc7aa1e180f8c23",
"Name":"Cinema 1",
"TimeSlots":[
{
"Start":"2018-07-29T16:00:00.000Z",
"End":"2018-07-29T18:30:00.000Z",
"Movie":{
"_id":"5b5dd8932dc7aa1e180f8c24",
"Name":"Movie 1"
}
},
{
"Start":"2018-07-29T15:00:00.000Z",
"End":"2018-07-29T17:15:00.000Z",
"Movie":{
"_id":"5b5dd8932dc7aa1e180f8c25",
"Name":"Movie 2"
}
}
]
}
{
"_id":"5b5dd8932dc7aa1e180f8c26",
"Name":"Cinema 2",
"TimeSlots":[
{
"Start":"2018-07-29T18:00:00.000Z",
"End":"2018-07-29T20:30:00.000Z",
"Movie":{
"_id":"5b5dd8932dc7aa1e180f8c24",
"Name":"Movie 1"
}
},
{
"Start":"2018-07-29T19:00:00.000Z",
"End":"2018-07-29T21:15:00.000Z",
"Movie":{
"_id":"5b5dd8932dc7aa1e180f8c25",
"Name":"Movie 2"
}
}
]
}
I'd like to reverse the tree structure (so I have movies that have timeslots, which are linked to cinemas). In C# using LINQ I would do the following:
var movies = cinemas
.SelectMany(cinema => cinema.TimeSlots)
.Select(timeSlot => timeSlot.Movie)
.Distinct(new MovieEqualityComparer())
.Select(movie =>
{
movie.TimeSlots = cinemas
.SelectMany(cinema => cinema.TimeSlots)
.Where(timeSlot => timeSlot.Movie.Id == movie.Id)
.ToList();
return movie;
})
.ToList();
I'm wondering how one would achive this using the aggregate functions and a aggregate pipeline. I've tried several options including $project, $unwind and $group but I don't seem to be able to achieve the result I want. Any help is appriciated.
This is what I have so far:
[
{
$unwind:{ path:'$TimeSlots' }
},
{
$unwind:{ path:'$TimeSlots.Movie' }
},
{
$group:{
_id:'$TimeSlots.Movie._id',
movies:{ $addToSet:'$TimeSlots.Movie' } }
},
{ $project: { movieId: '$_id'} } }
]
Which results into a similar result of my LINQ query up until the distinct.
No need to use double $unwind here since you have only one movie per timeslot. You can try below aggregation:
db.cinemas.aggregate([
{
$unwind: "$TimeSlots"
},
{
$group: {
_id: "$TimeSlots.Movie._id",
Name: { $first: "$TimeSlots.Movie.Name" },
TimeSlots: {
$push: {
Start: "$TimeSlots.Start",
End: "$TimeSlots.End",
Cinema: {
Name: "$Name",
_id: "$_id"
}
}
}
}
}
])
which outputs:
{
"_id" : "5b5dd8932dc7aa1e180f8c25",
"Name" : "Movie 2",
"TimeSlots" : [
{
"Start" : "2018-07-29T15:00:00.000Z",
"End" : "2018-07-29T17:15:00.000Z",
"Cinema" : {
"Name" : "Cinema 1",
"_id" : "5b5dd8932dc7aa1e180f8c23"
}
},
{
"Start" : "2018-07-29T19:00:00.000Z",
"End" : "2018-07-29T21:15:00.000Z",
"Cinema" : {
"Name" : "Cinema 2",
"_id" : "5b5dd8932dc7aa1e180f8c26"
}
}
]
}
{
"_id" : "5b5dd8932dc7aa1e180f8c24",
"Name" : "Movie 1",
"TimeSlots" : [
{
"Start" : "2018-07-29T16:00:00.000Z",
"End" : "2018-07-29T18:30:00.000Z",
"Cinema" : {
"Name" : "Cinema 1",
"_id" : "5b5dd8932dc7aa1e180f8c23"
}
},
{
"Start" : "2018-07-29T18:00:00.000Z",
"End" : "2018-07-29T20:30:00.000Z",
"Cinema" : {
"Name" : "Cinema 2",
"_id" : "5b5dd8932dc7aa1e180f8c26"
}
}
]
}