I am trying to build an extension method for IMongoCollection<TDocument>
in C# which will allow any List<TDocument>
to be upserted into a MongoDB collection using an upsert. I have found other articles that suggest a List<WriteModel<TDocument>>
, in combination with BulkWriteAsync to perform these operations in a batch.
In a non-generic manner, I can upsert a series of entries (in this case a List<Line>
) using:
public static async Task<BulkWriteResult<Line>> BulkUpsertAsyncNonGeneric(this IMongoCollection<Line> collection, List<Line> entries)
{
var bulkOps = new List<WriteModel<Line>>();
foreach (var entry in entries)
{
var filter = Builders<Line>.Filter.Eq(doc => doc.Id, entry.Id);
var upsertOne = new ReplaceOneModel<Line>(filter, entry) { IsUpsert = true };
bulkOps.Add(upsertOne);
}
return await collection.BulkWriteAsync(bulkOps);
}
By changing <Line>
for <TDocument>
I have made this partly generic, but there is an assumption that every TDocument has an Id
field and that every entry in entries
also has an Id
field. Of course, TDocument
has no members. I want to make these field definitions fully generic, ideally using a lambda to match the format of the call to Filter.Eq(doc => doc.Id, entry.Id)
. However, I'm stuck. I really want to avoid simply passing a string literal with the field names, which I believe would work fine but isn't compile-time safe.
I've come up with the following, which unsurprisingly does not compile:
public static async Task<BulkWriteResult<TDocument>> BulkUpsertAsync<TDocument, TField>(this IMongoCollection<TDocument> collection, List<TDocument> entries, Expression<Func<TDocument, TField>> filterField, Expression<Func<TDocument, TField>> valueField)
{
var bulkOps = new List<WriteModel<TDocument>>();
foreach (var entry in entries)
{
var filter = Builders<TDocument>.Filter.Eq(filterField, valueField);
var upsertOne = new ReplaceOneModel<TDocument>(filter, entry) { IsUpsert = true };
bulkOps.Add(upsertOne);
}
return await collection.BulkWriteAsync(bulkOps);
}
I suspect the type of valueField is incorrect, but additionally the compiler complains that
Error CS1503: Argument 1: cannot convert from
'System.Linq.Expressions.Expression<System.Func<TDocument, TField>>'
to'MongoDB.Driver.FieldDefinition<TDocument, System.Linq.Expressions.Expression<System.Func<TDocument, TField>>>'
(21, 48)
I managed to get this working using a compiled lambda.
The
Expression<Func<TDocument, TField>>
in the method signature lets me point at a property or field onTDocument
, which is used in both the creation of the filter for MongoDB, as well as the in the iterator that creates theReplaceOneModel
for eachentry in entries
.It can be called as follows:
Hope this helps someone!