[CSHARP-1922] Filter with ReplaceOneAsync resuls in NullReferenceException Created: 16/Feb/17  Updated: 08/Mar/17  Resolved: 08/Mar/17

Status: Closed
Project: C# Driver
Component/s: API
Affects Version/s: 2.4.2
Fix Version/s: 2.4.3

Type: Bug Priority: Major - P3
Reporter: Matthew O'Connell Assignee: Robert Stam
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Depends
depends on CSHARP-1894 InvalidCastException in FieldValueSer... Closed

 Description   

NullReferenceException results from the following code after upgrade to v2.4.2:

System.NullReferenceException occurred
Message: Exception thrown: 'System.NullReferenceException' in MongoDB.Bson.dll
Additional information: Object reference not set to an instance of an object.

Here's our C# code:

    public class FxRateDataDb : IFxRateDataDb
    {
        [BsonId]
        public string Id { get; set; }
}
 
    public class MongoDbCollection<T, TId> : IDbCollection<T> where T : IId<TId>
    {
        private readonly IMongoCollection<T> _mongoCollection;
 
        public async Task ReplaceOneAsync(T item, bool upsert)
        {
            await _mongoCollection.ReplaceOneAsync(x => Equals(x.Id, item.Id), item, new UpdateOptions {IsUpsert = upsert}); // NullReferenceException
        }
    }

And here's the stack trace:

>	MongoDB.Bson.dll!MongoDB.Bson.Serialization.IBsonSerializerExtensions.Serialize(MongoDB.Bson.Serialization.IBsonSerializer serializer, MongoDB.Bson.Serialization.BsonSerializationContext context, object value)	Unknown
 	MongoDB.Bson.dll!MongoDB.Bson.Serialization.IBsonSerializerExtensions.ToBsonValue(MongoDB.Bson.Serialization.IBsonSerializer serializer, object value)	Unknown
 	MongoDB.Driver.dll!MongoDB.Driver.Linq.Translators.PredicateTranslator.TranslateComparison(System.Linq.Expressions.Expression variableExpression, System.Linq.Expressions.ExpressionType operatorType, System.Linq.Expressions.ConstantExpression constantExpression)	Unknown
 	MongoDB.Driver.dll!MongoDB.Driver.Linq.Translators.PredicateTranslator.Translate(System.Linq.Expressions.Expression node)	Unknown
 	MongoDB.Driver.dll!MongoDB.Driver.Linq.Translators.PredicateTranslator.Translate(System.Linq.Expressions.Expression node, MongoDB.Bson.Serialization.IBsonSerializerRegistry serializerRegistry)	Unknown
 	MongoDB.Driver.dll!MongoDB.Driver.MongoCollectionImpl<SPMO.Providers.MarketData.FxRateDataDb>.ConvertWriteModelToWriteRequest(MongoDB.Driver.WriteModel<SPMO.Providers.MarketData.FxRateDataDb> model, int index)	Unknown
 	System.Core.dll!System.Linq.Enumerable.SelectIterator<MongoDB.Driver.WriteModel<SPMO.Providers.MarketData.FxRateDataDb>, MongoDB.Driver.Core.Operations.WriteRequest>(System.Collections.Generic.IEnumerable<MongoDB.Driver.WriteModel<SPMO.Providers.MarketData.FxRateDataDb>> source, System.Func<MongoDB.Driver.WriteModel<SPMO.Providers.MarketData.FxRateDataDb>, int, MongoDB.Driver.Core.Operations.WriteRequest> selector)	Unknown
 	MongoDB.Driver.Core.dll!MongoDB.Driver.Core.Operations.BulkMixedWriteOperation.BatchHelper.FindOrderedRuns(int maxRunLength)	Unknown
 	MongoDB.Driver.Core.dll!MongoDB.Driver.Core.Misc.ReadAheadEnumerable<MongoDB.Driver.Core.Operations.BulkMixedWriteOperation.Run>.ReadAheadEnumerator.MoveNext()	Unknown
 	MongoDB.Driver.Core.dll!MongoDB.Driver.Core.Operations.BulkMixedWriteOperation.BatchHelper.GetBatches()	Unknown
 	MongoDB.Driver.Core.dll!MongoDB.Driver.Core.Operations.BulkMixedWriteOperation.ExecuteAsync(MongoDB.Driver.Core.Bindings.IWriteBinding binding, System.Threading.CancellationToken cancellationToken)	Unknown
 	mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)	Unknown



 Comments   
Comment by Robert Stam [ 08/Mar/17 ]

With that version of the test program I can reproduce the NullReferenceException with 2.4.2. The extra levels of indirection must be what made the difference.

The NullReferenceException is gone in 2.4.3.

Note that by declaring the Id using an interface the serialized form of the Id value includes a "_t" discriminator value.

Your work around is fine for 2.4.2, and should no longer be necessary in 2.4.3.

Comment by Paul Reed [ 08/Mar/17 ]

Try this:

 
using System;
using System.ComponentModel;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
 
namespace TestCSharp1922
{
    public interface IAuditTrailKey
    {
        [BsonElement("D", Order = 0)]
        DateTime KeyDateTime { get; set; }
        [BsonElement("S", Order = 1)]
        string KeyString { get; set; }
        [BsonElement("I", Order = 2)]
        int KeyInt { get; set; }
    }
 
    public class AuditTrailEntryId : IAuditTrailKey
    {
        [BsonElement("D", Order = 0)]
        public DateTime KeyDateTime { get; set; }
        [BsonElement("S", Order = 1)]
        public string KeyString { get; set; }
 
        [BsonElement("I", Order = 2)]
        public int KeyInt { get; set; }
 
        public override string ToString()
        {
            return $"{KeyDateTime}:{KeyString}:{KeyInt}";
        }
    }
 
    public interface IAuditTrail<TKey> where TKey : IAuditTrailKey
    {
        [BsonId()]
        TKey Id { get; set; }
    }
 
    public class AuditTrailEntry : IAuditTrail<IAuditTrailKey>
    {
 
        [BsonId()]
        public IAuditTrailKey Id { get; set; }
    }
 
    public static class Program
    {
        public static void Main(string[] args)
        {
 
            var id = new AuditTrailEntryId
            {
                KeyDateTime = DateTime.UtcNow,
                KeyString = "abc",
                KeyInt = 123
            };
            var entry = new AuditTrailEntry() { Id = id };
 
 
            DoMethod<AuditTrailEntry,IAuditTrailKey>(entry);
        }
 
        public static void DoMethod<TAuditTrail,TKey>(TAuditTrail entry) where TAuditTrail : IAuditTrail<TKey>
        where TKey : IAuditTrailKey
        {
            var client = new MongoClient("mongodb://localhost");
            var database = client.GetDatabase("test");
            var collection = database.GetCollection<TAuditTrail>("test");
 
            var options = new UpdateOptions { IsUpsert = true };
            // Works..
            var filter = new FilterDefinitionBuilder<TAuditTrail>().Eq(tad => tad.Id, entry.Id);
            var result = collection.ReplaceOneAsync(filter, entry, options).GetAwaiter().GetResult();
 
            // Fails..
            var filter2 = new ExpressionFilterDefinition<TAuditTrail>(tad => tad.Id.Equals(entry.Id));
            var result2 = collection.ReplaceOneAsync(filter2, entry, options).GetAwaiter().GetResult();
        }
    }
}

Comment by Robert Stam [ 08/Mar/17 ]

paul.reed I am unable to reproduce any error with your example. I get no exception and the resulting filter looks correct.

I had to guess at some code to get something that compiled and ran. This is what I tested with. Let me know if I need to change anything to reproduce the error you are seeing. I tested against 2.4.2.

using System;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
 
namespace TestCSharp1922
{
    public interface IAuditTrailKey
    {
        DateTime KeyDateTime { get; set; }
        string KeyString { get; set; }
        int KeyInt { get; set; }
    }
 
    public class AuditTrailEntryId : IAuditTrailKey
    {
        [BsonElement("D", Order = 0)]
        public DateTime KeyDateTime { get; set; }
        [BsonElement("S", Order = 1)]
        public string KeyString { get; set; }
 
        [BsonElement("I", Order = 2)]
        public int KeyInt { get; set; }
 
        public override string ToString()
        {
            return $"{KeyDateTime}:{KeyString}:{KeyInt}";
        }
    }
 
    public class C
    {
        public AuditTrailEntryId Id { get; set; }
    }
 
    public static class Program
    {
        public static void Main(string[] args)
        {
            var client = new MongoClient("mongodb://localhost");
            var database = client.GetDatabase("test");
            var collection = database.GetCollection<C>("test");
 
            var id = new AuditTrailEntryId
            {
                KeyDateTime = DateTime.UtcNow,
                KeyString = "abc",
                KeyInt = 123
            };
            var entry = new C { Id = id };
            
            var filter = new ExpressionFilterDefinition<C>(tad => tad.Id.Equals(entry.Id));
            var options = new UpdateOptions { IsUpsert = true };
            var result = collection.ReplaceOneAsync(filter, entry, options).GetAwaiter().GetResult();
        }
    }
}

Comment by Paul Reed [ 08/Mar/17 ]

With my example shown - I get the exact same error. So I think it is the same issue.
Do you not find that

 await _mongoCollection.ReplaceOneAsync(new FilterDefinitionBuilder<T>().Eq( tad=>tad.Id, item.Id), item, new UpdateOptions {IsUpsert = upsert}); // NullReferenceException

fixes the issue ?

Comment by Robert Stam [ 08/Mar/17 ]

paul.reed I was not able to reproduce your issue.

In any case, it looks like it might be something different. If you can provide more information to reproduce please open a new ticket as the original issue reported in this ticket has been fixed and this ticket is closed.

Comment by Robert Stam [ 08/Mar/17 ]

I can also confirm that this has the same underlying issue as CSHARP-1894 which is fixed in 2.4.3, so I am closing this ticket as fixed in 2.4.3.

Comment by Robert Stam [ 08/Mar/17 ]

I gave another try at reproducing this, this time using code as close as yours as possible. I had to add a few minor things to get it to compile.

I can now reproduce the NullReferenceException in 2.4.2, and can confirm that there was no NullReferenceException in 2.4.1. So I am changing the affects version back to 2.4.2.

My code is:

using System.Threading.Tasks;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
 
namespace TestCSharp1922
{
    public interface IId<out T>
    {
        T Id { get; }
    }
 
    public interface IFxRateDataDb : IId<string>
    {
    }
 
    public class FxRateDataDb : IFxRateDataDb
    {
        [BsonId]
        public string Id { get; set; }
    }
 
    public class Program
    {
        public static void Main(string[] args)
        {
            var client = new MongoClient("mongodb://localhost");
            var database = client.GetDatabase("test");
            var collection = database.GetCollection<FxRateDataDb>("test");
 
            var mongoDbCollection = new MongoDbCollection<FxRateDataDb, string>(collection);
            var item = new FxRateDataDb { Id = "abc" };
            mongoDbCollection.ReplaceOneAsync(item, upsert: true).GetAwaiter().GetResult();
        }
 
    }
 
    public class MongoDbCollection<T, TId> where T : IId<TId>
    {
        private readonly IMongoCollection<T> _mongoCollection;
 
        public MongoDbCollection(IMongoCollection<T> collection)
        {
            _mongoCollection = collection;
        }
 
        public async Task ReplaceOneAsync(T item, bool upsert)
        {
            await _mongoCollection.ReplaceOneAsync(x => Equals(x.Id, item.Id), item, new UpdateOptions { IsUpsert = upsert }); // NullReferenceException
        }
    }
}

Comment by Paul Reed [ 08/Mar/17 ]

This worked for me - and I found it in 2.4.2

Replace:

                result = col.ReplaceOneAsync(new ExpressionFilterDefinition<T>(tad => tad.Id.Equals(entry.Id)), entry, options);

With:

                result = col.ReplaceOneAsync(new FilterDefinitionBuilder<T>().Eq( tad=>tad.Id, entry.Id), entry, options);

So I am guessing an issue with ExpressionFilterDefinition.
My Id was a composite:

 
    public class AuditTrailEntryId : IAuditTrailKey
    {
        [BsonElement("D", Order=0)] public DateTime KeyDateTime { get; set; }
        [BsonElement("S", Order = 1)] public string KeyString { get; set; }
 
        [BsonElement("I", Order = 2)] public int KeyInt { get; set; }
 
        public override string ToString()
        {
            return $"{KeyDateTime}:{KeyString}:{KeyInt}";
        }
    }

Comment by Matthew O&#39;Connell [ 21/Feb/17 ]

Downgrading back to 2.2.3 in the meantime.

Comment by Matthew O&#39;Connell [ 21/Feb/17 ]

Yeah, actually the suggested work-around is what we had in the codebase originally - same NullReferenceException.

None of our id's are ints. They are all either strings or Guids.

[edit: the Guids are stored as strings too]

Comment by Robert Stam [ 16/Feb/17 ]

Also, I get the exception in the previous comment when I use IId<int>, but not when I use IId<string>.

Comment by Robert Stam [ 16/Feb/17 ]

Also, I don't get a NullReferenceException when I attempt to reproduce this. I get:

Unhandled Exception: System.ArgumentException: Expression of type 'System.Int32' cannot be used for parameter of type 'System.Object' of method 'Boolean Equals(System.Object, System.Object)'

I don't think we support expressions that use the static Object.Equals method.

I've changed the affects version to 2.3 because this doesn't seem to be an issue introduced in 2.4.2.

Comment by Robert Stam [ 16/Feb/17 ]

As a workaround for now it looks like you could use:

x => x.Id.Equals(item.Id)

instead of:

x => Equals(x.Id, item.Id)

As the first parameter to ReplaceOneAsync.

Comment by Matthew O&#39;Connell [ 16/Feb/17 ]

Unable to edit the sample code - it would suffice here if FxRateDataDb inherited IId<string>, where:

  public interface IId<out T>
  {
    T Id { get; }
  }

Generated at Wed Feb 07 21:41:04 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.