Mongodb How to Update many documents using C# driver - c#

Please be so kind to help.
I got List of documents from xml parsing service and trying update it in DB.
I create fiter builder like .
var filter = Builders<T>.Filter.In("Id", List<T>);
and update builder like.
var update = Builders<T>.Update.Set("T.Property", List<T> )
and using UpdateManyAsync() updating documents in DB, but changings not apply.
How I could update documents in 1 step ?

Hello this is a sample using a .NET core 3.1 console application.
This is the csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.13.1" />
</ItemGroup>
</Project>
This is the code inside of the Program.cs file:
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MongoUpdateMany
{
public static class Program
{
public static async Task Main(string[] args)
{
const string databaseName = "test";
const string collectionName = "students";
var client = new MongoClient();
var database = client.GetDatabase(databaseName);
var collection = database.GetCollection<Student>(collectionName);
// just to be sure the test data are clean, nothing to do with the update sample
await database.DropCollectionAsync(collectionName).ConfigureAwait(false);
// create a bunch of students
var id = 1;
var enrico = new Student()
{
Name = "Enrico",
Id = id++,
IsActive = false
};
var luca = new Student
{
Name = "Luca",
Id = id++,
IsActive = false
};
var giulia = new Student
{
Name = "Giulia",
Id = id++,
IsActive = true
};
// fill the collection
await collection.InsertManyAsync(new List<Student> { enrico, giulia, luca }).ConfigureAwait(false);
// update many
var ids = new List<int> { enrico.Id, luca.Id };
var filter = Builders<Student>
.Filter
.In(x => x.Id, ids);
var update = Builders<Student>
.Update
.Set(x => x.IsActive, true);
await collection.UpdateManyAsync(filter, update).ConfigureAwait(false);
// verify updating the docs worked
await collection
.Find(student => ids.Contains(student.Id))
.ForEachAsync(student => Console.WriteLine($"Name: {student.Name} IsActive: {student.IsActive}"))
.ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine("Press enter to close...");
Console.ReadLine();
}
}
public class Student
{
[BsonId]
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
}
Here is some useful links to learn how to use the official C# driver for mongodb:
driver documentation
free course from the Mongo university. I highly encourage you to take this course: courses available in the Mongo university catalog are of very high quality.

Related

Mongo error when trying to update nested arrays: No array filter found for identifier

I am trying to update a document in Mongo that represents a community with the following scenario.
A community has a collection of blocks
A block has a collection of floors
A floor has a collection of doors
A door has a collection of label names
Given a document Id and information about the labels that must be placed into each door, I want to use the MongoDb C# driver v2.10.4 and mongo:latest to update nested lists (several levels).
I've reading the documentation, about array filters, but I can't have it working.
I've created a repository from scratch to reproduce the problem, with instructions on the Readme on how to run the integration test and a local MongoDB with docker.
But as a summary, my method groupds the labels so that I can bulk place names on the desired door and then it iterates over these groups and updates on Mongo the specific document setting the desired value inside some levels deep nested object. I couldn't think of a more efficient way.
All the code in the above repo.
The DB document:
public class Community
{
public Guid Id { get; set; }
public IEnumerable<Block> Blocks { get; set; } = Enumerable.Empty<Block>();
}
public class Block
{
public string Name { get; set; } = string.Empty;
public IEnumerable<Floor> Floors { get; set; } = Enumerable.Empty<Floor>();
}
public class Floor
{
public string Name { get; set; } = string.Empty;
public IEnumerable<Door> Doors { get; set; } = Enumerable.Empty<Door>();
}
public class Door
{
public string Name { get; set; } = string.Empty;
public IEnumerable<string> LabelNames = Enumerable.Empty<string>();
}
The problematic method with array filters:
public async Task UpdateDoorNames(Guid id, IEnumerable<Label> labels)
{
var labelsGroupedByHouse =
labels
.ToList()
.GroupBy(x => new { x.BlockId, x.FloorId, x.DoorId })
.ToList();
var filter =
Builders<Community>
.Filter
.Where(x => x.Id == id);
foreach (var house in labelsGroupedByHouse)
{
var houseBlockName = house.Key.BlockId;
var houseFloorName = house.Key.FloorId;
var houseDoorName = house.Key.DoorId;
var names = house.Select(x => x.Name).ToList();
var update =
Builders<Community>
.Update
.Set($"Blocks.$[{houseBlockName}].Floors.$[{houseFloorName}].Doors.$[{houseDoorName}].LabelNames", names);
await _communities.UpdateOneAsync(filter, update);
}
}
The exception is
MongoDB.Driver.MongoWriteException with the message "A write operation resulted in an error.
No array filter found for identifier 'Block 1' in path 'Blocks.$[Block 1].Floors.$[Ground Floor].Doors.$[A].LabelNames'"
Here's a more visual sample on how the nested structure looks like in the database. Notice the value I want to update is the LabelNames, which is an array of string.
I appreciate any help to have this working and suggestions on whether it's the right approach assuming that I cannot change the repository's method signature.
SOLUTION RESULT:
Thanks for the quick answer #mickl, it works perfectly.
Result at this repo's specific point of history exactly as suggested.
The $[{houseBlockName}] expects an identifier which acts as a placeholder and has a corresponding filter defined within arrayfilters (positional filtered). It seems like you're trying to pass the filter value directly which is incorrect.
Your C# code can look like this:
var houseBlockName = house.Key.BlockId;
var houseFloorName = house.Key.FloorId;
var houseDoorName = house.Key.DoorId;
var names = house.Select(x => x.Name).ToList();
var update = Builders<Community>.Update.Set("Blocks.$[block].Floors.$[floor].Doors.$[door].LabelNames", names);
var arrayFilters = new List<ArrayFilterDefinition>();
ArrayFilterDefinition<BsonDocument> blockFilter = new BsonDocument("block.Name", new BsonDocument("$eq", houseBlockName));
ArrayFilterDefinition<BsonDocument> floorFilter = new BsonDocument("floor.Name", new BsonDocument("$eq", houseFloorName));
ArrayFilterDefinition<BsonDocument> doorFilter = new BsonDocument("door.Name", new BsonDocument("$eq", houseDoorName));
arrayFilters.Add(blockFilter);
arrayFilters.Add(floorFilter);
arrayFilters.Add(doorFilter);
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var result = _communities.UpdateOne(filter, update, updateOptions);
{
var filterCompany = Builders<CompanyInfo>.Filter.Eq(x => x.Id, Timekeepping.CompanyID);
var update = Builders<CompanyInfo>.Update.Set("LstPersonnel.$[i].Timekeeping.$[j].CheckOutDate", DateTime.UtcNow);
var arrayFilters = new List<ArrayFilterDefinition>
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("i.MacAddress",new BsonDocument("$eq", Timekeepping.MacAddress) )),
new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("j.Id", new BsonDocument("$eq", timeKeeping.Id)))
};
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters};
var updateResult = await _companys.UpdateOneAsync(filterCompany, update, updateOptions);
return updateResult.ModifiedCount != 0;
}

CsvHelper PrepareHeaderForMatch returns Context as one-item array

Been using CsvHelper version 6.0.0, decided to upgrade to latest (currently 12.3.2) and found out it uses another parameter, index in lambda for csv.Configuration.PrepareHeaderForMatch, (Func<string,int,string>).
The code for v6.0.0 looked like this:
csv.Configuration.PrepareHeaderForMatch = header => Regex.Replace(header, #"\/", string.Empty);
With previous line, the IReadingContext.Record returns an array with multiple records, one for each column.
The code for v12.3.2 looks like this:
csv.Configuration.PrepareHeaderForMatch = (header, index) => Regex.Replace(header, #"\/", string.Empty);
But ReadingContext.Record now returns an array with all columns in just one record. Used the exact same file for both versions. Tried messing with the lambda, but the outcome is the same. How can I get the columns in Records array?
Thanks in advance!
update - This is an issue with the delimiter that has changed since version 6.0.0. The default delimiter now uses CultureInfo.CurrentCulture.TextInfo.ListSeparator. Since I'm in the United States, my ListSeparator is , so both examples work for me. For many countries the ListSeparator is ; which is why for version 12.3.2 only 1 column was found for #dzookatz. The solution is to specify the delimiter in the configuration.
csv.Configuration.PrepareHeaderForMatch = header => Regex.Replace(header, #"\/", string.Empty);
csv.Configuration.Delimiter = ",";
I must be missing something. I get the same result for var record whether using version 6.0.0 or 12.3.2. I'm guessing there is more going on with your data that I'm not seeing.
Version 6.0.0
class Program
{
static void Main(string[] args)
{
var fooString = $"Id,First/Name{Environment.NewLine}1,David";
using (var reader = new StringReader(fooString))
using (var csv = new CsvReader(reader))
{
csv.Configuration.PrepareHeaderForMatch = header => Regex.Replace(header, #"\/", string.Empty);
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
var record = csv.Context.Record;
}
}
}
}
public class Foo
{
public int Id { get; set; }
public string FirstName { get; set; }
}
Version 12.3.2
public class Program
{
public static void Main(string[] args)
{
var fooString = $"Id,First/Name{Environment.NewLine}1,David";
using (var reader = new StringReader(fooString))
using (var csv = new CsvReader(reader))
{
csv.Configuration.PrepareHeaderForMatch = (header, index) => Regex.Replace(header, #"\/", string.Empty);
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
var record = csv.Context.Record;
}
}
}
}
public class Foo
{
public int Id { get; set; }
public string FirstName { get; set; }
}

How can i update document in mongo db using .net(c#) driver without using Builders?

I am using MongoDb .net driver in which i have to update document based on certain condition.
Here is how my find query looks like in c# mongo driver
DbService.conversations.Find (new BsonDocument ("_id", new ObjectId ("obje-id-here"))).FirstOrDefault ();
How can i update specific field of document based on certain _id using Mongodb .net driver without using Builders?
**Note : **
I have tried this update query
var updateResultFromQuery = await DbService.conversations.UpdateOneAsync(Builders<RawBsonDocument>.Filter.Eq("_id", "5e01a89e5f317324780b7f83"),Builders<RawBsonDocument>.Update.Set("visitorName", "Guest41815"));
Console.WriteLine("after update response --- "+updateResultFromQuery.ToJson());
But it's not updating the value even i am receiving update response like this
{ "_t" : "Acknowledged" }
You could simply ReplaceOne object instead of updating it , that way you wont be forced to use builders but you will be doing a find and replace instead. 2 database operations instead of one. You can then update your object in memory instead.
Collection.ReplaceOne(filter, replacement, new UpdateOptions() { IsUpsert = false });
here's an example of create/update/find as requested:
using MongoDB.Entities; //Install-Package MongoDB.Entities
using MongoDB.Entities.Core;
using System;
using System.Linq;
namespace StackOverflow
{
public class Customer : Entity
{
public string Name { get; set; }
public Agent Agent { get; set; }
}
public class Agent
{
public string Name { get; set; }
public string Email { get; set; }
}
public class Program
{
private static void Main()
{
new DB("test", "localhost");
// create a customer with embedded agent
var customer = new Customer
{
Name = "Customer A",
Agent = new Agent { Name = "Agent Uno", Email = "uno#youknow.com" }
};
customer.Save();
// update customer name
DB.Update<Customer>()
.Match(c =>
c.ID == customer.ID &&
c.Agent.Email == "uno#youknow.com")
.Modify(c =>
c.Name, "Updated Customer")
.Execute();
// find updated customer
var cst = DB.Find<Customer>()
.Match(customer.ID)
.Execute()
.Single();
Console.WriteLine($"Customer Name: {cst.Name}");
Console.Read();
}
}
}
I achieved updating my document without using any third party library using these options
var filter = new BsonDocument(new Dictionary<string, dynamic> () {
{
"_id", new BsonObjectId("object-id-here")
},
{
"assigned_to.email" , agentEmail
}
});
var updateDoc = new BsonDocument(new Dictionary<string, dynamic> () {
{
"$set", new BsonDocument("assigned_to.$.avgResponseTime", Convert.ToDouble(obj.agentChatAvgResponseTime))
}
});
var updateQueryResult = DbService.conversations.UpdateOne(filter, updateDoc, new UpdateOptions {IsUpsert = false });

MongoDB as a lock with WriteConcern Majority and ReadConcern Linearizable

I got one place in my application where I want to use Mongo (3.6) as a lock of multiple threads (on different servers). Essentially something like "if one thread started work, other threads should see it through mongo and dont start the same work in parallel".
From the documentation I learned
Combined with "majority" write concern, "linearizable" read concern enables multiple threads to perform reads and writes on a single document as if a single thread performed these operations in real time;
So this sounded good to me, I insert a certain document if one thread starts work, and other threads check if such document already exists and dont start if so, but it does not work for my case.
I prepared two tests - one non-parallel that successfully blocks the second thread - but the parallel test fails and I get two of these RebuildLog documents.
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace FindOneAndUpdateTests
{
public class FindOneAndUpdateTests
{
private static IMongoDatabase GetDatabase()
{
var dbName = "test";
var client = new MongoClient("mongodb://localhost:45022");
return client.GetDatabase(dbName);
}
private IMongoCollection<RebuildLog> GetCollection()
{
return GetDatabase().GetCollection<RebuildLog>("RebuildLog");
}
[Fact]
public async Task FindOneAndUpdate_NotParallel_Test()
{
var dlpId = Guid.NewGuid();
var first = await FindOneAndUpdateMethod(dlpId);
var second = await FindOneAndUpdateMethod(dlpId);
first.Should().BeFalse();
second.Should().BeTrue();
}
[Fact]
public async Task FindOneAndUpdate_Parallel_Test()
{
var dlpId = Guid.NewGuid();
var taskFirst = FindOneAndUpdateMethod(dlpId);
var taskSecond = FindOneAndUpdateMethod(dlpId);
var first = await taskFirst;
var second = await taskSecond;
first.Should().BeFalse();
second.Should().BeTrue();
}
private async Task<bool> FindOneAndUpdateMethod(Guid dlpId)
{
var mongoCollection = GetCollection();
var filterBuilder = Builders<RebuildLog>.Filter;
var filter = filterBuilder.Where(w => w.DlpId == dlpId);
var creator = Builders<RebuildLog>.Update
.SetOnInsert(w => w.DlpId, dlpId)
.SetOnInsert(w => w.ChangeDate, DateTime.UtcNow)
.SetOnInsert(w => w.BuildDate, DateTime.UtcNow)
.SetOnInsert(w => w.Id, Guid.NewGuid());
var options = new FindOneAndUpdateOptions<RebuildLog>
{
IsUpsert = true,
ReturnDocument = ReturnDocument.Before
};
var result = await mongoCollection
.WithWriteConcern(WriteConcern.WMajority)
.WithReadConcern(ReadConcern.Linearizable)
.FindOneAndUpdateAsync(filter, creator, options);
return result != null;
}
}
[BsonIgnoreExtraElements]
public class RebuildLog
{
public RebuildLog()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
public DateTime ChangeDate { get; set; }
public string ChangeUser { get; set; }
public Guid DlpId { get; set; }
public string Portal { get; set; }
public DateTime? BuildDate { get; set; }
}
}
My suspicion is that my idea with the atomic handcrafted GetOrInsert (see the FindOneAndUpdate with IsUpsert) breaks the constraint of "on a single document" in the documentation. Any idea to fix this or is it just not possible?
It is interesting. May be you have no unique index on DlpId? That's why mongo decides that sequential execution of these operations is not necessary because in your case it's no write-then-read pattern (as it pointed in "Client Sessions and Causal Consistency Guarantees"). It is update-or-create two times concurrently.
What about this? :
public class SyncDocument
{
// ...
[BsonElement("locked"), BsonDefaultValue(false)]
public bool Locked { get; set; }
}
In client code:
var filter = Builders<SyncDocument>.Filter.Eq(d => d.Locked, false);
var update = Builders<SyncDocument>.Update.Set(d => d.Locked, true);
var result = collection.UpdateOne(filter, update);
if (result.ModifiedCount == 1) {
Console.WriteLine("Lock acquired");
}
Document with Locked field should be created before applications startup (if it is applicable for your task).

Mongodb Convention packs

How does one use a MongoDB ConventionPack in C# I have the following code:
MongoDatabase Repository = Server.GetDatabase(RepoName);
this.Collection = Repository.GetCollection<T>(CollectionName);
var myConventions = new ConventionPack();
myConventions.Add(new CamelCaseElementNameConvention());
Does the convention pack automatically attach to this.Collection? When I load in a new object will it automatically persist it as this case? Do I have to add tags in my class declaration (like a data contract)?
You need to register the pack in the ConventionRegistry:
var pack = new ConventionPack();
pack.Add(new CamelCaseElementNameConvention());
ConventionRegistry.Register("camel case",
pack,
t => t.FullName.StartsWith("Your.Name.Space."));
If you want to apply this globally, you can replace the last param with something simpler like t => true.
Working sample code that serializes and de-serializes (driver 1.8.20, mongodb 2.5.0):
using System;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;
namespace playground
{
class Simple
{
public ObjectId Id { get; set; }
public String Name { get; set; }
public int Counter { get; set; }
}
class Program
{
static void Main(string[] args)
{
MongoClient client = new MongoClient("mongodb://localhost/test");
var db = client.GetServer().GetDatabase("test");
var collection = db.GetCollection<Simple>("Simple");
var pack = new ConventionPack();
pack.Add(new CamelCaseElementNameConvention());
ConventionRegistry.Register("camel case", pack, t => true);
collection.Insert(new Simple { Counter = 1234, Name = "John" });
var all = collection.FindAll().ToList();
Console.WriteLine("Name: " + all[0].Name);
}
}
}

Categories

Resources