[CSHARP-2113] Expression based queries don't work for properties on nested interface types Created: 27/Nov/17  Updated: 28/Oct/23  Resolved: 22/Nov/22

Status: Closed
Project: C# Driver
Component/s: LINQ3
Affects Version/s: None
Fix Version/s: 2.19.0

Type: Bug Priority: Major - P3
Reporter: Daniel Hegener Assignee: Robert Stam
Resolution: Fixed Votes: 1
Labels: triaged
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

When using the expression based query API against a property on a (nested) interface type the driver fails with an exception. The below code demonstrates the issue:

using System;
using System.Linq.Expressions;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Linq.Translators;
using Xunit;
 
namespace MongoDB.Driver.Tests.Linq.Translators
{
    public class InterfaceTranslatorTests
    {
        #region Failing scenario
        public interface IMakeTheTestFail
        {
            int I { get; set; }
        }
 
        public class FailingType : IMakeTheTestFail
        {
            public IMakeTheTestFail Inner { get; set; }
            public int I { get; set; }
        }
 
        [Fact]
        public void Fails()
        {
            Assert(
                (IMakeTheTestFail x) => x.I == 1,
                "{'I': 1}");
            Assert(
                (FailingType x) => x.Inner.I == 1,
                "{'Inner.I': 1}");
        }
        #endregion
 
        #region Working scenario
        public class WorkingType
        {
            public WorkingType Inner { get; set; }
            public int I { get; set; }
        }
 
        [Fact]
        public void Works()
        {
            Assert(
                (WorkingType x) => x.I == 1,
                "{'I': 1}");
            Assert(
                (WorkingType x) => x.Inner.I == 1,
                "{'Inner.I': 1}");
        }
        #endregion
 
        public void Assert<T>(Expression<Func<T, bool>> filter, string expectedFilter)
        {
            var serializer = BsonSerializer.SerializerRegistry.GetSerializer<T>();
            var filterDocument = PredicateTranslator.Translate(filter, serializer, BsonSerializer.SerializerRegistry);
 
            filterDocument.Should().Be(BsonDocument.Parse(expectedFilter));
        }
    }
}

It all comes down to the fact that the nested property "Inner" will be automatically assigned a serializer of type DiscriminatedInterfaceSerializer which does not implement IBsonDocumentSerializer which again is fair enough since it shouldn't really know about the concretes mappings for properties on an unknown implementor of its covered interface... However, for querying purposes it would appear desirable to have that functionality in place.

The problem starts when SerializationBinder.VisitMember(MemberExpression node) performs the following type check:

                    var documentSerializer = serializationExpression.Serializer as IBsonDocumentSerializer;
                    BsonSerializationInfo memberSerializationInfo;
                    if (documentSerializer != null && documentSerializer.TryGetMemberSerializationInfo(node.Member.Name, out memberSerializationInfo))

As a result of this check, in the above example, this will cause the filter expression to be translated into "{(

{document}.I == 1)}" as opposed to "{({document}

.*{I}* == 1)}" for the working case which again causes PredicateTranslator.GetFieldExpression(Expression expression) to not find a matching field and fail:

System.InvalidOperationException
{document}{Inner}.I is not supported.
   at MongoDB.Driver.Linq.Translators.PredicateTranslator.GetFieldExpression(Expression expression) in C:\Users\daniel.hegener\Source\Repos\mongo-csharp-driver2\src\MongoDB.Driver\Linq\Translators\PredicateTranslator.cs:line 1637



 Comments   
Comment by Robert Stam [ 22/Nov/22 ]

This issue has been fixed in the new LINQ provider (known as LINQ3), which was introduced in the 2.14 release.

Configure your MongoClientSettings to use LinqProvider.V3 if you want to use this functionality.

To configure a client to use the LINQ3 provider use code like the following

var connectionString = "mongodb://localhost";
var clientSettings = MongoClientSettings.FromConnectionString(connectionString);
clientSettings.LinqProvider = LinqProvider.V3;
var client = new MongoClient(clientSettings);

Comment by Githook User [ 22/Nov/22 ]

Author:

{'name': 'rstam', 'email': 'robert@robertstam.org', 'username': 'rstam'}

Message: CSHARP-2113: Support LINQ queries against interfaces..
Branch: master
https://github.com/mongodb/mongo-csharp-driver/commit/148d7092244a378fc7b1840616553a51ae2dea07

Comment by James Kovacs [ 10/Feb/22 ]

Repro'd with LINQ3:

using MongoDB.Driver;
using MongoDB.Driver.Linq;
 
var settings = new MongoClientSettings { LinqProvider = LinqProvider.V3 };
var client = new MongoClient(settings);
var db = client.GetDatabase("test");
 
var query1 = db.GetCollection<WorkingType>("test").AsQueryable()
               .Where(x => x.I == 1);
Console.WriteLine(query1);
 
var query2 = db.GetCollection<WorkingType>("test").AsQueryable()
               .Where(x => x.Inner.I == 1);
Console.WriteLine(query2);
 
var query3 = db.GetCollection<IMakeTheTestFail>("test").AsQueryable()
               .Where(x => x.I == 1);
Console.WriteLine(query3);
 
var query4 = db.GetCollection<FailingType>("test").AsQueryable()
               .Where(x => x.Inner.I == 1);
Console.WriteLine(query4);
 
public interface IMakeTheTestFail
{
    int I { get; set; }
}
 
public class FailingType : IMakeTheTestFail
{
    public IMakeTheTestFail Inner { get; set; }
    public int I { get; set; }
}
 
public class WorkingType
{
    public WorkingType Inner { get; set; }
    public int I { get; set; }
}

Output is:

test.test.Aggregate([{ "$match" : { "I" : 1 } }])
test.test.Aggregate([{ "$match" : { "Inner.I" : 1 } }])
Unable to cast object of type 'MongoDB.Bson.Serialization.Serializers.DiscriminatedInterfaceSerializer`1[IMakeTheTestFail]' to type 'MongoDB.Bson.Serialization.IBsonDocumentSerializer'.
Serializer for IMakeTheTestFail must implement IBsonDocumentSerializer to be used with LINQ.

Comment by Daniel Hegener [ 27/Nov/17 ]

One "fix" could be similar to the following change (in SerializationBinder.VisitMember(MemberExpression node)) - this "fix", however, causes problems in other locations of the code (at least some other failing unit tests):

                    if (documentSerializer != null && documentSerializer.TryGetMemberSerializationInfo(node.Member.Name, out memberSerializationInfo))
                    {
                        if (memberSerializationInfo.ElementName == null)
                        {
                            newNode = new DocumentExpression(memberSerializationInfo.Serializer);
                        }
                        else
                        {
                            newNode = new FieldExpression(
                                mex.Expression,
                                memberSerializationInfo.ElementName,
                                memberSerializationInfo.Serializer,
                                mex);
                        }
                    }
                    else
                    {
                        // this is just to support queries against interface properties
                        newNode = new FieldExpression(
                            mex.Expression,
                            node.Member.Name,
                            _bindingContext.SerializerRegistry.GetSerializer(node.Type),
                            mex);
                    }

Comment by Daniel Hegener [ 27/Nov/17 ]

I'm sorry, I cannot edit my above issue anymore but the error I posted is a little bit misleading since it's for an older version of my test code. The real exception message for the test as posted above will not be

{document}{Inner}.I is not supported.

but
{document}

.I is not supported.

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