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);
Related
I'm trying to update just a few nested documents on my .Net Application, but I don't know exactly how to do that.
Here is one of the topics I've been searching after making this question:
C# - MongoDB - Update an element inside a Nested Document
**DataBase Object: **
{
"_id" : ObjectId("5d03a6f5fba276260c4911bd"),
"costumerId" : ObjectId("5c050cdefba2771eac58c84b"),
"order" : 106376,
"items" : [
{
"itemId" : 10226905,
"date" : ISODate("2018-11-30T14:00:00.000Z"),
"description" : "itemA",
"new" : true,
},
{
"itemId" : 10226906,
"date" : ISODate("2018-11-30T14:00:00.000Z"),
"description" : "itemB",
"new" : true,
},
{
"itemId" : 10226907,
"date" : ISODate("2018-11-23T14:00:00.000Z"),
"description" : "ItemC",
"new" : true,
},
{
"itemId" : 10226908,
"date" : ISODate("2018-11-23T14:00:00.000Z"),
"description" : "ItemD",
"new" : false,
},
]
}
MongoDB Update Query:
db.CostumerProducts.update(
{costumerId: 106376},
{
$set:{'items.$[element].new': true}
},
{
multi:true,
arrayFilters: [
{
'element.itemId':{
$in:[10226905,10226906,10226907]
}
}]
})
C# Update Query using MongoDB driver:
var arrayObjects = List<int>{10226905,10226906,10226907};
var filter = Builders<CostumerProductsObject>.Filter.And(new[]
{
new FilterDefinitionBuilder<CostumerProductsObject>().Eq(p=>p.costumerId, costumerId),
new FilterDefinitionBuilder<CostumerProductsObject>().Eq(p=>p.order, order),
});
var set = Builders<ItemsObject>.Update.Set("items.$[element].new", true);
var arrayFilter = new FilterDefinitionBuilder<ItemsObject>().In(a=>a.itemId, itemsArray);
using (var db = new MongoContextInfra())
{
db.Set<CostumerProductsObject>(CollectionName).UpdateMany(filter,
set,
new UpdateOptions
{
ArrayFilters = null
});
}
I expect to update just the three items into "arrayObjects" variable.
Can you show me some code or help me fixing mine so I can make this update?
thanks.
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 have a collection Zonedetails shown below. below.
I am using C# to Insert or Update a Unit to Units array. If it is an insert I can insert Area blank. If it is an update It should only update the UnitName.
{
"Code" : "Zone1",
"Name" : "ZoneName1",
"Units" : [
{
"UnitCode" : "Unitcode1",
"UnitName" : "UnitCodeName",
"Areas" : [
{
"AreaCode" : "AreaCode1",
"AreaName" : "AreaName1"
}
]
}
]
}
{
"Code" : "Zone2",
"Name" : "ZoneName2",
"Units" : [
{
"UnitCode" : "UnitCode2",
"UnitName" : "UnityName2",
"Areas" : [
{
"AreaCode" : "Areacode2",
"AreaName" : "AreaName2"
}
]
}
]
}
{
"Code" : "Zone3",
"Name" : "ZoneName3",
"Units" : [
{
"UnitCode" : "UnitCode3",
"UnitName" : "UnitName3",
"Areas" : [
{
"AreaCode" : "Areadcode3",
"AreaName" : "AreaName3"
},
{
"AreaCode" : "AreaCode4",
"AreaName" : "Areaname4"
},
{
"AreaCode" : "AreaCode5",
"AreaName" : "Areaname5"
}
]
},
{
"UnitCode" : "UnitCode6",
"UnitName" : "UnitName6",
"Areas" : [
{
"AreaCode" : "AreaCode10",
"AreaName" : "AreaName10"
},
{
"AreaCode" : "AreaCOde11",
"AreaName" : "AreaName10"
},
{
"AreaCode" : "AreaCode12",
"AreaName" : "AreaName12"
}
]
}
]
}
I have writtent a C# code shown below. But getting "The positional operator did not find the match needed from the query"error if the Unit Code does not exist. Added not before unitCode check.
var mongoCollection = _context.GetCollection<Zone>("ZoneDetail");
var filter = Builders<Zone>.Filter.Where(x => x.Code == zoneCode && !x.Units.Any(u => u.UnitCode == unit.UnitCode));
var updateUnitCode = Builders<Zone>.Update.Set(x => x.Units.ElementAt(-1).UnitCode, unit.UnitCode);
var updateUnitName = Builders<Zone>.Update.Set(x => x.Units.ElementAt(-1).UnitName, unit.UnitName);
var result = await mongoCollection.UpdateOneAsync(filter, Builders<Zone>.Update.Combine(updateUnitCode, updateUnitName), new UpdateOptions { IsUpsert = true});
Your error message suggest that filter is not able to find matching element.
Either try to change your input or change your filter criteria.
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("...") } }
] }
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"
}
}
]
}