MongoDB updateMany set a boolean field using equals - c#

I have an IsDefault field in my model, and I want to set it to true for one document only and false for the others. Is this possible in a single query? I tried
{ $set: { IsDefault: {$eq: [_id, id ]}}}
{ $set: { IsDefault: { $cond: { if: { $eq: [$_id, id] }, then: true, else: false } } } }
and it didn't work. I also tried passing a lambda expression but it won't build either. Any idea if this is possible or do I need to use two seperate updateOne statements to the the current IsDefault to false, and the new one to true?

you can achieve it with an aggregation pipeline update like this:
db.collection.updateMany(
{},
[
{
$set: {
IsDefault: {
$eq: ["$_id", ObjectId("5f65c2fcaf29d00898173d03")]
}
}
}
])
here's a full example using mongodb.entities:
using MongoDB.Entities;
using System.Threading.Tasks;
namespace StackOverflow
{
public class Book : Entity
{
public string Title { get; set; }
public bool IsDefault { get; set; }
}
static class Program
{
private static async Task Main()
{
await DB.InitAsync("test", "localhost");
var books = new[] {
new Book { Title = "book 1"},
new Book { Title = "book 2"},
new Book { Title = "book 3"}
};
await books.SaveAsync();
var stage = new Template<Book>(#"
{
$set: {
IsDefault: {
$eq: ['$_id', ObjectId('<idvalue>')]
}
}
}")
.Tag("idvalue", books[1].ID);
await DB.Update<Book>()
.Match(_ => true)
.WithPipelineStage(stage)
.ExecutePipelineAsync();
}
}
}

Related

MongoDB - Cannot create field 'ChildProperty' in element {ParentProperty: is null}

I have a User class:
public class User
{
public Guid? Id { get; set; }
public String? Name { get; set; }
public Address? Address { get; set; }
}
and Address class:
public class Address
{
public String? Street { get; set; }
public String? City { get; set; }
public String? State { get; set; }
}
I am trying to implement partial updating. This is done dynamically, but let's say someone sends to update a User's City and State, I do:
var filter = Builders<BsonDocument>.Filter.Eq("Id", id);
var updates = new List<UpdateDefinition<BsonDocument>>();
updates.Add(Builders<BsonDocument>.Update.Set("Address.City", "New City Value"));
updates.Add(Builders<BsonDocument>.Update.Set("Address.State", "New State Value"));
var update = Builders<BsonDocument>.Update.Combine(updates);
var bsonDocument = await collection.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions<BsonDocument>
{
ReturnDocument = ReturnDocument.After
});
This works well, except if a User's Address is null. In that case, I get the error:
MongoDB.Driver.MongoCommandException: Command findAndModify failed:
Cannot create field 'City' in element {Address: null}.
Is there any way to ensure the Address object is created so that the City and State properties get set? I would like to do it without getting the current object from the database.
Think that you may work with update with aggregation pipeline to update the Address field dynamically.
Work with $cond operator to check whether Address is null.
If yes, set the whole object to Address.
If no, merge the current Address value with the document.
The query below may looks complex:
db.collection.update({
Id: /* Id */
},
[
{
$set: {
Address: {
$cond: {
if: {
$eq: [
"$Address",
null
]
},
then: {
"City": "New City Value",
"State": "New State Value"
},
else: {
$mergeObjects: [
"$Address",
{
"City": "New City Value",
"State": "New State Value"
}
]
}
}
}
}
}
])
var addressDocument = new BsonDocument
{
{ "City", "New City Value" },
{ "State", "New State Value" }
};
var update = Builders<BsonDocument>.Update.Pipeline(new PipelineStagePipelineDefinition<BsonDocument, BsonDocument>
(
new PipelineStageDefinition<BsonDocument, BsonDocument>[]
{
new BsonDocument("$set",
new BsonDocument("Address",
new BsonDocument("$cond",
new BsonDocument
{
{
"if",
new BsonDocument("$eq",
BsonArray.Create(new object[] { "$Address", null }))
},
{
"then",
addressDocument
},
{
"else",
new BsonDocument("$mergeObjects",
BsonArray.Create(new object[] { "$Address", addressDocument }))
}
}
)
)
)
}
));

Mongodb C# Update element in an multiple array with multiple values

I want to update the single document in collection with the guid as filter and update value is cityType. Every guid has different citytype here i have used 3 types it may be more.
So please give a right implementation using c# code.
Models:
public class Country
{
[BsonId]
public ObjectId Id { get; set; }
public int CountryId {get; set; }
public IEnumerable<States> States { get; set; }
}
public class States
{
public Guid Guid { get; set; }
public CityType CityType { get; set; }
}
Enum CityType
{
Unknown = 0,
Rural = 1,
Urban = 2
}
Existing Collection:
{
"_id": ObjectId("6903ea4d2df0c5659334e763"),
"CountryId": 200,
"States": [
{
"Guid": "AFCC4BE7-7585-5E46-A639-52F0537895D8",
"CityType": 0,
},
{
"Guid": "208FB603-08C7-46D9-B0C0-7AF4F691A96D",
"CityType": 0,
}
}
Input:
List<States>()
{
new States()
{
Guid = "AFCC4BE7-7585-5E46-A639-52F0537895D8",
CityType = CityType.Rural
},
new States()
{
Guid = "208FB603-08C7-46D9-B0C0-7AF4F691A96D",
CityType = CityType.Urban
}
}
Expected:
{
"_id": ObjectId("6903ea4d2df0c5659334e763"),
"CountryId": 200,
"States": [
{
"Guid": "AFCC4BE7-7585-5E46-A639-52F0537895D8",
"CityType": 1,
},
{
"Guid": "208FB603-08C7-46D9-B0C0-7AF4F691A96D",
"CityType": 2,
}
}
This is the method I have tried:
public async Task<bool> UpdateType(int countryId, IEnumerable<States> states)
{
var collection = connectionFactory.GetCollection<Country>(collectionName);
var cityTypes = states.Select(x => x.CityType);
var filter = Builders<Country>.Filter.Empty;
var update = Builders<Country>.Update.Set("States.$[edit].CityType", cityTypes);
var arrayFilters = new List<ArrayFilterDefinition>();
foreach (var state in states)
{
ArrayFilterDefinition<Country> optionsFilter = new BsonDocument("state.Guid", new BsonDocument("$eq", state.Guid));
arrayFilters.Add(optionsFilter);
}
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var result = await collection.UpdateOneAsync(filter, update, updateOptions);
return result;
}
hope all details I have added here. Thanks in advance.
You don't have to loop through it:
Let's say you have a Class1 like this:
class Question : AuditableEntity {
public string Text { get; set; }
public List<string> Tags { get; set; } = new List<string>();
so you just say:
await collection.UpdateOneAsync(
someFilter,
Builders<Class1>.Update
.Set(f => f.Text, request.Question.Text)
.Set(f => f.Tags, request.Question.Tags));

Match "x out of y" fields in a document

I'm writing a service to handle bans for a game and I'm currently a bit stuck trying to write a MongoDB query. Currently I have a collection of "User" objects, and the objects look like this:
public class User
{
public List<Ban> Bans { get; set; }
// some irrelevant additional fields
}
public class Ban
{
public HardwareId HWID { get; set; }
public DateTime Expires { get; set; }
// some irrelevant additional fields
}
public class HardwareId : IEquatable<HardwareId>
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public string Field4 { get; set; }
public bool Equals([AllowNull] HardwareId other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(this, other)) return true;
return Field1 == other.Field1 &&
Field2 == other.Field2 &&
Field3 == other.Field3 &&
Field4 == other.Field4;
}
}
What I want to do is have a query that finds all users with a ban where HWID has say 3 out of 4 fields matching. Currently I have a query that only finds users where the HWID match exactly (due to the Equals() implementation), but I'd like to change it. My current code looks like this:
public Ban FindBan(HardwareId hwid)
{
var banBuilder = Builders<Ban>.Filter;
var hwidFilter = banBuilder.Eq(b => b.HWID, hwid);
var expFilter = banBuilder.Gt(b => b.Expires, DateTime.UtcNow);
var banFilter = banBuilder.And(hwidFilter, expFilter);
var user = _users.Find(Builders<User>.Filter.ElemMatch(p => p.Bans, banFilter)).FirstOrDefault();
if (user != null)
{
return user.Bans[0];
}
return null;
}
The only way of solving it I can think of is to write "spaghetti if-statements" in the Equals() function, but I'd like a dynamic solution where I could add multiple "Fields" to the HardwareId class later down the line. Another issue with the FindBan() function currently is the fact that it returns "user.Bans[0]" instead of the actual ban that was found, but I imagine I could solve that by sorting by expiry.
here's one approach to match by minimum of 3 fields of HWID:
var numFieldsToMatch = 3;
var hwid = {
Field1: "one",
Field2: "two",
Field3: "three",
Field4: "four"
};
db.User.find({
$expr: {
$anyElementTrue: {
$map: {
input: "$Bans",
in: {
$gte: [{
$size: {
$filter: {
input: { $objectToArray: "$$this.HWID" },
cond: {
$or: [
{ $and: [{ $eq: ["$$this.k", "Field1"] }, { $eq: ["$$this.v", hwid.Field1] }] },
{ $and: [{ $eq: ["$$this.k", "Field2"] }, { $eq: ["$$this.v", hwid.Field2] }] },
{ $and: [{ $eq: ["$$this.k", "Field3"] }, { $eq: ["$$this.v", hwid.Field3] }] },
{ $and: [{ $eq: ["$$this.k", "Field4"] }, { $eq: ["$$this.v", hwid.Field4] }] }
]
}
}
}
}, numFieldsToMatch]
}
}
}
}
})
https://mongoplayground.net/p/MPwB14o6kuO
it's not possible to translate this mongo query to c# driver code afaik.
see here for an easy way to run this with c#.
You can write a method similar to Equals (or make changes to it), so that you have each condition match in to an array
FldArr[0] = (Field1 == other.Field1);
FldArr[1] = (Field2 == other.Field2);
FldArr[2] = (Field3 == other.Field3);
FldArr[3] = (Field4 == other.Field4);
Then by using LINQ for FldArr, you can find 3/4 true conditions.

Mongodb C# driver update all subarray elements

I want to update a document and set a value to an array of subdocument.
Using the documentation I have to use the $[] operator.
Following this link it is now possible to do stuff like this :
db.coll.update({}, {$set: {“a.$[].b”: 2}})
Input: {a: [{b: 0}, {b: 1}]}
Output: {a: [{b: 2}, {b: 2}]}
For example this request will do the job in my case :
db.collection.update(
{ "History": { "$elemMatch": { "status": { "$ne": "PROCESSED" } } } },
{ "$set": { "History.$[].flag": false } },
{ "multi": true }
)
But I do not find the way to do the $[] operator in C# with the driver.
And the driver documentation does not contain the information.
Can someone please provide me a C# sample.
you can achieve it like this:
collection.UpdateMany(
x => x.History.Any(h => h.status != "PROCESSED"),
Builders<YourType>.Update.Set("History.$[].flag", false));
here's an alternative strongly-typed solution:
using MongoDB.Entities;
using MongoDB.Entities.Core;
using System.Linq;
namespace StackOverflow
{
public class Test : Entity
{
public Event[] History { get; set; }
}
public class Event
{
public bool flag { get; set; }
public string status { get; set; }
}
public class Program
{
private static void Main(string[] args)
{
new DB("test", "localhost");
(new[] {
new Test { History = new[]{
new Event { flag = true, status = "PROCESSED" } } },
new Test { History = new[]{
new Event { flag = true, status = "NOT-PROCESSED" },
new Event { flag = true, status = "NOT-PROCESSED" }
}}
}).Save();
var field = Prop.PosAll<Test>(t => t.History[0].flag);
DB.Update<Test>()
.Match(t => t.History.Any(h => h.status != "PROCESSED"))
.Modify(b => b.Set(field, false))
.Execute();
}
}
}
Let's suppose that you have an object called History:
public class History : MongoDocument
{
// here you have some other properties, and you have a list of objects
public Guid Guid { get; private set; }
public List<SOME_OBJECT> NAME_OF_THE_ARRAY { get; set; }
}
And SOME_OBJECT:
public class SOME_OBJECT
{
// here you have other properties
public bool Flag { get; set; }
public string Name { get; set; }
}
And you want to update all objects into NAME_OF_THE_ARRAY:
public async Task<bool> Update_NAME_OF_THE_ARRAY(string id)
{
var filter = Builders<History>.Filter.Eq("_id", ObjectId.Parse(id));
var update = Builders<History>.Update.Combine(
Builders<History>.Update.Set(x => x.NAME_OF_THE_ARRAY[-1].Name, "test")
Builders<History>.Update.Set(x => x.NAME_OF_THE_ARRAY[-1].Flag, false);
var result = await _historyCollection.UpdateOneAsync(filter, update);
return result.ModifiedCount > 0;
}

MongoDB HashTable Averages

I am using c#,along with MongoDB.
I have a class that can be resembled by this.
Its a sample, that represents something, please dont comment on the class design
[CollectionName("Venues")]
public class Venue
{
public string Name { get; set; }
public dictionary<string,object> Properties { get; set; }
}
var venue = new Venue
{
Name = "Venue 1",
Properties = new Dictionary<string,object>
{
{ "Chairs", "18" },
{ "Tables", "4" },
{ "HasWaterfall", true }
}
}
Assuming I have an object in a collection, that looks like that.
I would like to find out of it is possible to do two things.
1: Load from the database, only a single item from the dictionary,
currently I can only see how this can be done, by loading the entire
record from the database and then manually getting the value by key.
2: Determine the average of a single item within the database.
For example, across all records I would like to work out the average
chairs, again without loading all records and then doing it in memory with
linq etc....
Basically your sample document gets stored as a below JSON:
{
"_id" : ObjectId("..."),
"Name" : "Venue 1",
"Properties" : {
"Chairs" : "18",
"Tables" : "4",
"HasWaterfall" : true
}
}
This gives you a possibility to define a projection using dot notation:
var filter = Builders<Venue>.Filter.Eq(f => f.Name, "Venue 1");
var projection = Builders<Venue>.Projection.Include("Properties.Chairs");
List<BsonDocument> data = Col.Find(filter).Project(projection).ToList();
which returns below following BsonDocument:
{ "_id" : ObjectId("..."), "Properties" : { "Chairs" : "18" } }
To get the average you need to use $toInt operator introduced in MongoDB 4.0 to convert your values from string to int. Try:
var project = new BsonDocument()
{
{ "chairs", new BsonDocument() { { "$toInt", "$Properties.Chairs" } } }
};
var group = new BsonDocument()
{
{ "_id", "null" },
{ "avg", new BsonDocument() { { "$avg", "$chairs" } } }
};
var avg = Col.Aggregate().Project(project).Group(group).First();
here's an alternative way of doing it using MongoDB.Entities convenience library.
using System.Collections.Generic;
using System.Linq;
using MongoDB.Entities;
namespace StackOverflow
{
class Program
{
[Name("Venues")]
public class Venue : Entity
{
public string Name { get; set; }
public Dictionary<string, object> Properties { get; set; }
}
static void Main(string[] args)
{
new DB("test");
var venue1 = new Venue
{
Name = "Venue 1",
Properties = new Dictionary<string, object> {
{ "Chairs", 28 },
{ "Tables", 4 },
{ "HasWaterfall", true }
}
};
venue1.Save();
var venue2 = new Venue
{
Name = "Venue 2",
Properties = new Dictionary<string, object> {
{ "Chairs", 38 },
{ "Tables", 4 },
{ "HasWaterfall", true }
}
};
venue2.Save();
var chairs = DB.Find<Venue, object>()
.Match(v => v.Name == "Venue 1")
.Project(v => new { ChairCount = v.Properties["Chairs"] })
.Execute();
var avgChairs = DB.Collection<Venue>()
.Average(v => (int)v.Properties["Chairs"]);
}
}
}
results in the following queries being made to the database:
getting chairs in venue 1:
db.runCommand({
"find": "Venues",
"filter": {
"Name": "Venue 1"
},
"projection": {
"Properties.Chairs": NumberInt("1"),
"_id": NumberInt("0")
},
"$db": "test"
})
getting average chair count across all venues:
db.Venues.aggregate([
{
"$group": {
"_id": NumberInt("1"),
"__result": {
"$avg": "$Properties.Chairs"
}
}
}
])

Categories

Resources