'MongoDb id generator is not working

I have a simple need generate string ID if field is null before inserting. It works fine if property has name Id, but otherwise it doesn't.

I have following class:

public abstract class CampaignBase
{
   [BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
   [BsonRepresentation(BsonType.ObjectId)]
   public string CampaignId { get; set; }
}

public class Campaign : CampaignBase {}

Now when I insert MyData in the database I get null instead of generated id. It seems that these attributes just are not applied, because if property has Id name then if works fine and attribute can change actual data layout (string/objectid/etc).

This is how I save it:

enter image description here

campaign and campaignBase are referencing to the same object, so don't mind it.

Where UpdateOptions:

protected static UpdateOptions UpdateOptions => new UpdateOptions
{
    IsUpsert = true
};

And here it is: null is arriving:

enter image description here

Am I missing something?



Solution 1:[1]

I have ended up with following extension:

public static void Save<T, TProperty>(this IMongoCollection<T> collection, T item, Expression<Func<T, TProperty>> idFunc) where TProperty : class
{
    var id = idFunc.Compile()(item);
    if (id == null)
    {
        collection.InsertOne(item);
    }
    else
    {
        var expression = Expression.Lambda<Func<T, bool>>(Expression.Equal(idFunc.Body, Expression.Constant(id, typeof(TProperty))), idFunc.Parameters);
        collection.ReplaceOne(expression, item);
    }
}

Sample usage:

CampaignsCollection.Save(campaign, c => c.CampaignId);

Thank @KevinSmith for idea


Here is a bit more complicated implementation, but it's more resilent, it has better client interface and performance due to caching

public static class MongoExtensions
{
    public static void Save<T>(this IMongoCollection<T> collection, T item)
    {
        if (item == null)
            throw new ArgumentNullException(nameof(item));

        if (MongoSaveCommandHelper<T>.ShouldInsert(item))
        {
            collection.InsertOne(item);
        }
        else
        {
            var expression = MongoSaveCommandHelper<T>.GetIdEqualityExpression(item);
            collection.ReplaceOne(expression, item);
        }
    }

    private static class MongoSaveCommandHelper<T>
    {
        private static readonly Expression<Func<T, bool>> IdIsEqualToDefaultExpression;
        private static readonly Func<T, object> GetId;
        public static Func<T, bool> ShouldInsert { get; }

        static MongoSaveCommandHelper()
        {
            var members = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
            var idProperty = members.SingleOrDefault(x => x.IsDefined(typeof(BsonIdAttribute)))
                             ?? members.FirstOrDefault(m => m.Name.Equals("id", StringComparison.OrdinalIgnoreCase));
            if (idProperty == null)
                throw new InvalidOperationException("Id property has not found");

            var idPropertyType = idProperty.PropertyType;
            var parameter = Expression.Parameter(typeof(T));
            var idPropertyAccess = Expression.MakeMemberAccess(parameter, idProperty);
            var getIdFuncExpression = Expression.Lambda<Func<T, object>>(Expression.Convert(idPropertyAccess, typeof(object)), parameter);

            GetId = getIdFuncExpression.Compile();
            IdIsEqualToDefaultExpression = Expression.Lambda<Func<T, bool>>(Expression.Equal(idPropertyAccess, Expression.Default(idPropertyType)), getIdFuncExpression.Parameters);
            ShouldInsert = IdIsEqualToDefaultExpression.Compile();
        }

        public static Expression<Func<T, bool>> GetIdEqualityExpression(T item) => 
            (Expression<Func<T, bool>>)new IdConstantVisitor(GetId(item)).Visit(IdIsEqualToDefaultExpression);
    }

    private class IdConstantVisitor : ExpressionVisitor
    {
        private readonly object _value;
        public IdConstantVisitor(object value) => _value = value;
        protected override Expression VisitDefault(DefaultExpression node) => Expression.Constant(_value, node.Type);
    }
}

Generally, it just insert an item if Id field is equal to default(PropertyType), otherwise it replace an item with specified id.

Here it is how we can use it:

CampaignsCollection.Save(campaign);

This code handles everything for us. No mistaken invalid ID column, no extra typing, just save it, period :)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1