2

What I want to achieve is to provide List<JobList> containing client Id and InternalIds of jobs to the Finish() method. Then I want to iterate through clients and update all jobs matches JobInternalIds and set current datetime to FinishedAt field. The problem is that I totally don't know how to update nested objects.

I've tried something like below but without success.

public class Client
{
    [BsonId]
    public ObjectId Id { get; set;}
    public string Name { get; set;}
    public List<Job> Jobs { get; set;}
}

public class Job
{
    public string InternalId { get; set;}
    public string Name { get; set;}
    public DateTime? FinishedAt { get; set;}
}

public class JobList
{
    public string ClientId { get; set; }
    List<string> JobInternalIds { get; set; }
}

public async Task Finish(List<JobList> joblist)
{   
    var updateDefinition = new UpdateDefinitionBuilder<Client>()
        .Set(x => x.Job[0].FinishedAt, DateTime.UtcNow); // don't know how to set datetime to finishedAt for each elements of collection that matches InternalIds

    foreach(var item in joblist)
    {
        await _db.Collection.UpdateManyAsync(item.ClientId, updateDefinition);
    }   
}

----------EDITED----------
public async Task Finish(List<JobList> joblist)
{
    var updateDefinition = Builders<Client>.Update
        .Set(x => x.Jobs[-1].FinishedAt, DateTime.UtcNow);

    foreach (var item in joblist)
    {
        var internalIds = item.JobInternalIds.Select(x => x.InternalId).ToList();   
        var idFilter = Builders<Client>.Filter.Eq(x => x.Id, item.ClientId);
        var internalIdsFilter = Builders<Client>.Filter.ElemMatch(x => x.Jobs, y => internalIds.Contains(y.InternalId));
        var combinedFilters = Builders<Client>.Filter.And(idFilter, internalIdsFilter);

        await _db.Collection.UpdateManyAsync(combinedFilters, updateDefinition);
    }
}
byasu
  • 33
  • 1
  • 4

1 Answers1

2

You can use the filtered positional operator to update all elements that match a condition in an array:

var internalIds = new[] { "1", "2" };
var idFilter = Builders<Client>.Filter.Eq(x => x.Id, objectId);
var updateDefinition = Builders<Client>.Update
    .Set("Jobs.$[job].FinishedAt", DateTime.UtcNow);

await collection.UpdateManyAsync(idFilter, updateDefinition,
     new UpdateOptions
     {
         ArrayFilters = new []
         {
             new BsonDocumentArrayFilterDefinition<BsonDocument>(new BsonDocument("job.groupName", new BsonDocument("$in", new BsonArray(internalIds)))),
         }
     });

Check out this blog post for some examples - https://kevsoft.net/2020/03/23/updating-arrays-in-mongodb-with-csharp.html

Kevin Smith
  • 13,746
  • 4
  • 52
  • 77
  • Thanks for answer, is it possible to add additional condition inside update definition to update only jobs where InternalId equals to something? I would like to update only specific jobs inside JobList. – byasu Mar 21 '20 at 17:29
  • Yes, for that you'd need to build a filter that matches that condition and then uses the positional operator instead ($). There's even a typed way to do that in C# too. `Builders.Update.Set( x=> x.Jobs[-1].FinishedAt, DateTime.UtcNow)` – Kevin Smith Mar 22 '20 at 22:32
  • Hey Kevin, Could you tell me what I'm doing wrong here? I think that filter is working correctly because it's updating correct element but it updating just first matching element instead of all. So if I have two matching elements I have to run Finish() method twice. – byasu Mar 23 '20 at 10:04
  • Ah gotcha, you'll need to use array filters if you want to update only certain ones in the array - https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/#up._S_[%3Cidentifier%3E] – Kevin Smith Mar 23 '20 at 10:10
  • Thank you, it's working well. I definitely need to increase my knowledge of mongo! :) – byasu Mar 25 '20 at 11:58